This commit is contained in:
Nouman Tahir 2022-10-17 11:28:45 +05:00
parent d34fa30f40
commit 3c63b00f87
132 changed files with 5256 additions and 5569 deletions

View File

@ -3,14 +3,13 @@ import AppCenter from 'appcenter';
import { name as appName } from './app.json';
import 'core-js';
import 'intl';
import 'intl/locale-data/jsonp/en-US'
import 'intl/locale-data/jsonp/en-US';
// set check frequency options
const EcencyApp = require('./App').default;
AppCenter.setLogLevel(AppCenter.LogLevel.VERBOSE);
// TODO Remove ignoreLogs when referenced issue is fixed properly
// ref: https://github.com/ecency/ecency-mobile/issues/2466
// ignore warnings

View File

@ -1,12 +1,27 @@
import React, { useEffect, useRef, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { Alert } from 'react-native';
import { useIntl } from 'react-intl';
import { navigate } from '../../../navigation/service';
import { removeOtherAccount, updateCurrentAccount } from '../../../redux/actions/accountAction';
import { isPinCodeOpen, isRenderRequired, login, logout, logoutDone } from '../../../redux/actions/applicationActions';
import {
isPinCodeOpen,
isRenderRequired,
login,
logout,
logoutDone,
} from '../../../redux/actions/applicationActions';
import { getUserDataWithUsername, removeAllUserData, removePinCode, setAuthStatus, setExistUser, setPinCodeOpen } from '../../../realm/realm';
import {
getUserDataWithUsername,
removeAllUserData,
removePinCode,
setAuthStatus,
setExistUser,
setPinCodeOpen,
} from '../../../realm/realm';
import {
migrateToMasterKeyWithAccessToken,
refreshSCToken,
@ -20,12 +35,10 @@ import { toggleAccountsBottomSheet } from '../../../redux/actions/uiAction';
import AUTH_TYPE from '../../../constants/authType';
import { getDigitPinCode, getMutes } from '../../../providers/hive/dhive';
import { setFeedPosts, setInitPosts } from '../../../redux/actions/postsAction';
import { Alert } from 'react-native';
import { useIntl } from 'react-intl';
import { useAppSelector } from '../../../hooks';
import { getUnreadNotificationCount } from '../../../providers/ecency/ecency';
import { decryptKey } from '../../../utils/crypto';
import { getPointsSummary} from '../../../providers/ecency/ePoint';
import { getPointsSummary } from '../../../providers/ecency/ePoint';
import { fetchSubscribedCommunities } from '../../../redux/actions/communitiesAction';
import { clearSubscribedCommunitiesCache } from '../../../redux/actions/cacheActions';
@ -71,7 +84,6 @@ const AccountsBottomSheetContainer = ({ navigation }) => {
dispatch(logout());
};
const _handleSwitch = async (switchingAccount = {}) => {
try {
const accountData = accounts.filter(
@ -94,8 +106,12 @@ const AccountsBottomSheetContainer = ({ navigation }) => {
//migreate account to use access token for master key auth type
if (realmData[0].authType !== AUTH_TYPE.STEEM_CONNECT && realmData[0].accessToken === '') {
_currentAccount = await migrateToMasterKeyWithAccessToken(_currentAccount, realmData[0], pinHash);
}
_currentAccount = await migrateToMasterKeyWithAccessToken(
_currentAccount,
realmData[0],
pinHash,
);
}
//refresh access token
const encryptedAccessToken = await refreshSCToken(
@ -112,10 +128,8 @@ const AccountsBottomSheetContainer = ({ navigation }) => {
dispatch(updateCurrentAccount(_currentAccount));
dispatch(clearSubscribedCommunitiesCache());
dispatch(fetchSubscribedCommunities(_currentAccount.username))
}
catch(error){
dispatch(fetchSubscribedCommunities(_currentAccount.username));
} catch (error) {
Alert.alert(
intl.formatMessage({
id: 'alert.fail',

View File

@ -1,2 +1,2 @@
export * from './optionsModal';
export * from './progressBar'
export * from './progressBar';

View File

@ -1,17 +1,17 @@
import EStyleSheet from 'react-native-extended-stylesheet';
export default EStyleSheet.create({
container:{
backgroundColor:'$primaryLightBackground',
flexDirection:'row',
borderRadius:16,
height:16,
alignSelf:'stretch',
marginHorizontal:8,
marginBottom:12,
},
filled:{
borderRadius:16,
backgroundColor:'$primaryBlue'
},
container: {
backgroundColor: '$primaryLightBackground',
flexDirection: 'row',
borderRadius: 16,
height: 16,
alignSelf: 'stretch',
marginHorizontal: 8,
marginBottom: 12,
},
filled: {
borderRadius: 16,
backgroundColor: '$primaryBlue',
},
});

View File

@ -1,20 +1,15 @@
import React from 'react';
import { View } from "react-native"
import { View } from 'react-native';
import styles from '../children/progresBarStyles';
export const ProgressBar = ({
progress
}) => {
const containerStyle = {...styles.container};
const filledStyle = {...styles.filled, flex:progress};
const unfilledStyle = {flex:100 - progress}
return (
<View style={containerStyle}>
<View style={filledStyle} />
<View style={unfilledStyle} />
</View>
)
}
export const ProgressBar = ({ progress }) => {
const containerStyle = { ...styles.container };
const filledStyle = { ...styles.filled, flex: progress };
const unfilledStyle = { flex: 100 - progress };
return (
<View style={containerStyle}>
<View style={filledStyle} />
<View style={unfilledStyle} />
</View>
);
};

View File

@ -1 +1 @@
export * from './container/progressBar';
export * from './container/progressBar';

View File

@ -1,63 +1,59 @@
import React, { useEffect, useState } from "react";
import { Image } from "react-native";
import EStyleSheet from "react-native-extended-stylesheet";
import FastImage from "react-native-fast-image";
import { TouchableOpacity } from "react-native-gesture-handler";
import React, { useEffect, useState } from 'react';
import { Image } 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,
imgUrl:string,
isAnchored:boolean,
activeOpacity?:number,
onPress:()=>void,
}
interface AutoHeightImageProps {
contentWidth: number;
imgUrl: string;
isAnchored: boolean;
activeOpacity?: number;
onPress: () => void;
}
export const AutoHeightImage = ({
contentWidth,
imgUrl,
isAnchored,
activeOpacity,
onPress,
}: AutoHeightImageProps) => {
const [imgWidth, setImgWidth] = useState(contentWidth);
const [imgHeight, setImgHeight] = useState(imgWidth * (9 / 16));
const [onLoadCalled, setOnLoadCalled] = useState(false);
export const AutoHeightImage = ({
contentWidth,
imgUrl,
isAnchored,
activeOpacity,
onPress
}:AutoHeightImageProps) => {
useEffect(() => {
_fetchImageBounds();
}, []);
const _fetchImageBounds = () => {
Image.getSize(imgUrl, (width, height) => {
const newWidth = width < contentWidth ? width : contentWidth;
const newHeight = (height / width) * newWidth;
setImgHeight(newHeight);
setImgWidth(newWidth);
});
};
const [imgWidth, setImgWidth] = useState(contentWidth);
const [imgHeight, setImgHeight] = useState(imgWidth * (9/16))
const [onLoadCalled, setOnLoadCalled] = useState(false);
const imgStyle = {
width: imgWidth - 10,
height: imgHeight,
backgroundColor: onLoadCalled ? 'transparent' : EStyleSheet.value('$primaryGray'),
};
useEffect(() => {
_fetchImageBounds();
}, [])
const _onLoad = () => {
setOnLoadCalled(true);
};
const _fetchImageBounds = () => {
Image.getSize(imgUrl, (width, height)=>{
const newWidth = width < contentWidth ? width : contentWidth;
const newHeight = (height / width) * newWidth;
setImgHeight(newHeight);
setImgWidth(newWidth);
})
}
const imgStyle = {
width:imgWidth - 10,
height:imgHeight,
backgroundColor: onLoadCalled ? 'transparent' : EStyleSheet.value('$primaryGray')
}
const _onLoad = () => {
setOnLoadCalled(true);
}
return (
<TouchableOpacity onPress={onPress} disabled={isAnchored} activeOpacity={activeOpacity || 1}>
<FastImage
style={imgStyle}
source={{uri:imgUrl}}
resizeMode={FastImage.resizeMode.contain}
onLoad={_onLoad}
/>
</TouchableOpacity>
)
}
return (
<TouchableOpacity onPress={onPress} disabled={isAnchored} activeOpacity={activeOpacity || 1}>
<FastImage
style={imgStyle}
source={{ uri: imgUrl }}
resizeMode={FastImage.resizeMode.contain}
onLoad={_onLoad}
/>
</TouchableOpacity>
);
};

View File

@ -9,12 +9,11 @@ import { setHidePostsThumbnails } from '../../../redux/actions/applicationAction
// Components
import BasicHeaderView from '../view/basicHeaderView';
interface BackHeaderProps {
backIconName:'close'|'arrow-back';
backIconName: 'close' | 'arrow-back';
}
const BasicHeaderContainer = (props:BackHeaderProps) => {
const BasicHeaderContainer = (props: BackHeaderProps) => {
const dispatch = useAppDispatch();
const isHideImages = useAppSelector((state) => state.application.hidePostsThumbnails);

View File

@ -3,6 +3,7 @@ import { View, Text, ActivityIndicator, SafeAreaView } from 'react-native';
import { injectIntl } from 'react-intl';
// Components
import EStyleSheet from 'react-native-extended-stylesheet';
import { TextButton } from '../..';
import { IconButton } from '../../iconButton';
import { DropdownButton } from '../../dropdownButton';
@ -12,7 +13,6 @@ import { TextInput } from '../../textInput';
// Styles
import styles from './basicHeaderStyles';
import { OptionsModal } from '../../atoms';
import EStyleSheet from 'react-native-extended-stylesheet';
const BasicHeaderView = ({
disabled,
@ -48,11 +48,9 @@ const BasicHeaderView = ({
handleSettingsPress,
backIconName,
}) => {
const [isInputVisible, setIsInputVisible] = useState(false);
const rewardMenuRef = useRef(null);
/**
*
* ACTION HANDLERS
@ -79,7 +77,6 @@ const BasicHeaderView = ({
handleOnSearch(value);
};
const _handleRewardMenuSelect = (index) => {
let rewardType = 'default';
@ -99,8 +96,6 @@ const BasicHeaderView = ({
}
};
/**
*
* UI RENDERER
@ -206,7 +201,10 @@ const BasicHeaderView = ({
onPress={() => handleOnSaveButtonPress && handleOnSaveButtonPress()}
/>
) : (
<ActivityIndicator style={styles.textButtonWrapper} color={EStyleSheet.value('$primaryBlue')} />
<ActivityIndicator
style={styles.textButtonWrapper}
color={EStyleSheet.value('$primaryBlue')}
/>
)}
</Fragment>
)}
@ -229,13 +227,15 @@ const BasicHeaderView = ({
text={rightButtonText}
/>
) : (
<ActivityIndicator style={[styles.textButtonWrapper]} color={EStyleSheet.value('$primaryBlue')} />
<ActivityIndicator
style={[styles.textButtonWrapper]}
color={EStyleSheet.value('$primaryBlue')}
/>
)}
</Fragment>
)}
</View>
<OptionsModal
ref={rewardMenuRef}
options={[
@ -248,7 +248,6 @@ const BasicHeaderView = ({
title="Reward"
onPress={_handleRewardMenuSelect}
/>
</SafeAreaView>
);
};

View File

@ -3,11 +3,11 @@ import { View, FlatList, Text, TouchableOpacity } from 'react-native';
import { useIntl } from 'react-intl';
import { isArray, debounce } from 'lodash';
import styles from './styles';
import EStyleSheet from 'react-native-extended-stylesheet';
import styles from './styles';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { BeneficiaryModal, CheckBox, FormInput, IconButton, TextButton } from '../../components';
import { BeneficiaryModal, CheckBox, FormInput, IconButton, TextButton } from '..';
import { Beneficiary } from '../../redux/reducers/editorReducer';
import { lookupAccounts } from '../../providers/hive/dhive';
import { TEMP_BENEFICIARIES_ID } from '../../redux/constants/constants';
@ -17,12 +17,11 @@ import {
} from '../../redux/actions/editorActions';
interface BeneficiarySelectionContentProps {
draftId: string;
setDisableDone: (value: boolean) => void;
powerDown?: boolean;
label?:string;
labelStyle?:string;
label?: string;
labelStyle?: string;
powerDownBeneficiaries?: Beneficiary[];
handleSaveBeneficiary?: (beneficiaries: Beneficiary[]) => void;
handleRemoveBeneficiary?: (beneficiary: Beneficiary) => void;
@ -38,7 +37,6 @@ const BeneficiarySelectionContent = ({
handleSaveBeneficiary,
handleRemoveBeneficiary,
}: BeneficiarySelectionContentProps) => {
const intl = useIntl();
const dispatch = useAppDispatch();
@ -76,7 +74,7 @@ const BeneficiarySelectionContent = ({
const readPowerDownBeneficiaries = () => {
const tempBeneficiaries = [
{ account: username, weight: 10000, autoPowerUp: false },
...powerDownBeneficiaries as Beneficiary[],
...(powerDownBeneficiaries as Beneficiary[]),
];
if (isArray(tempBeneficiaries) && tempBeneficiaries.length > 0) {
@ -96,8 +94,11 @@ const BeneficiarySelectionContent = ({
const readTempBeneficiaries = async () => {
if (beneficiariesMap) {
const savedBeneficiareis = beneficiariesMap[draftId || TEMP_BENEFICIARIES_ID];
const tempBeneficiaries = savedBeneficiareis && savedBeneficiareis.length ? [DEFAULT_BENEFICIARY, ...beneficiariesMap[draftId || TEMP_BENEFICIARIES_ID]] : [DEFAULT_BENEFICIARY];
const tempBeneficiaries =
savedBeneficiareis && savedBeneficiareis.length
? [DEFAULT_BENEFICIARY, ...beneficiariesMap[draftId || TEMP_BENEFICIARIES_ID]]
: [DEFAULT_BENEFICIARY];
if (isArray(tempBeneficiaries) && tempBeneficiaries.length > 0) {
//weight correction algorithm.
let othersWeight = 0;
@ -242,7 +243,7 @@ const BeneficiarySelectionContent = ({
onChange={(value) => _onWeightInputChange(value)}
selectTextOnFocus={true}
autoFocus={true}
returnKeyType={'next'}
returnKeyType="next"
keyboardType="numeric"
/>
</View>
@ -315,7 +316,7 @@ const BeneficiarySelectionContent = ({
beneficiaries[0].weight = beneficiaries[0].weight + item.weight;
const removedBeneficiary = beneficiaries.splice(index, 1);
setBeneficiaries([...beneficiaries]);
if(handleRemoveBeneficiary){
if (handleRemoveBeneficiary) {
handleRemoveBeneficiary(removedBeneficiary[0]);
return;
}
@ -364,7 +365,9 @@ const BeneficiarySelectionContent = ({
return (
<View style={styles.container}>
<Text style={labelStyle || styles.settingLabel}>{label || intl.formatMessage({ id: 'editor.beneficiaries' })}</Text>
<Text style={labelStyle || styles.settingLabel}>
{label || intl.formatMessage({ id: 'editor.beneficiaries' })}
</Text>
<FlatList
data={beneficiaries}
renderItem={_renderItem}

View File

@ -2,64 +2,63 @@ import EStyleSheet from 'react-native-extended-stylesheet';
import { getBottomSpace } from 'react-native-iphone-x-helper';
export default EStyleSheet.create({
sheetContent: {
backgroundColor: '$primaryBackgroundColor',
position:'absolute',
bottom:0,
left:0,
right:0,
zIndex:999
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
zIndex: 999,
},
thumbStyle: {
width: 72,
height: 72,
marginVertical: 8,
marginRight: 8,
borderRadius: 12,
backgroundColor: '$primaryLightGray',
},
selectedStyle: {
borderWidth: 4,
borderColor: '$primaryBlack',
},
settingLabel: {
color: '$primaryDarkGray',
fontSize: 14,
fontWeight: 'bold',
flexGrow: 1,
textAlign: 'left',
},
listContainer: {
paddingBottom: getBottomSpace() + 16,
},
container: {
paddingVertical: 16,
},
bodyWrapper: { flex: 1, paddingTop: 20, paddingBottom: 20 },
inputWrapper: { flexDirection: 'row', alignItems: 'center' },
contentLabel: { color: '$iconColor', marginTop: 4, textAlign: 'left' },
weightInput: { width: 80 },
weightFormInput: { flex: 1, color: '$primaryBlack', paddingLeft: 12 },
weightFormInputWrapper: { marginTop: 8 },
usernameInput: { flex: 1, color: '$primaryBlack', marginLeft: 16 },
usernameFormInputWrapper: { marginTop: 8, marginRight: 12 },
footerWrapper: { paddingTop: 16 },
saveButton: {
width: 140,
height: 44,
alignSelf: 'flex-end',
justifyContent: 'center',
},
doneButton: { borderRadius: 16, backgroundColor: '$primaryBlue' },
thumbSelectContainer: {
marginTop: 12,
},
checkBoxHeader: {
width: 50,
},
checkBoxContainer: {
width: 50,
marginTop: 12,
},
thumbStyle:{
width:72,
height:72,
marginVertical:8,
marginRight:8,
borderRadius:12,
backgroundColor:'$primaryLightGray'
},
selectedStyle:{
borderWidth:4,
borderColor:'$primaryBlack'
},
settingLabel:{
color: '$primaryDarkGray',
fontSize: 14,
fontWeight: 'bold',
flexGrow: 1,
textAlign:'left',
},
listContainer:{
paddingBottom:getBottomSpace() + 16,
},
container:{
paddingVertical:16
},
bodyWrapper: { flex: 1, paddingTop: 20, paddingBottom:20},
inputWrapper: { flexDirection: 'row', alignItems: 'center'},
contentLabel: { color: '$iconColor', marginTop:4, textAlign:'left' },
weightInput: {width:80},
weightFormInput: { flex:1, color: '$primaryBlack', paddingLeft: 12 },
weightFormInputWrapper: { marginTop: 8 },
usernameInput: { flex:1, color: '$primaryBlack', marginLeft: 16, },
usernameFormInputWrapper: { marginTop: 8, marginRight: 12 },
footerWrapper: { paddingTop:16 },
saveButton: {
width: 140,
height: 44,
alignSelf: 'flex-end',
justifyContent: 'center',
},
doneButton:{borderRadius:16, backgroundColor:'$primaryBlue'},
thumbSelectContainer:{
marginTop:12,
},
checkBoxHeader: {
width: 50,
},
checkBoxContainer: {
width: 50,
marginTop:12,
}
});

View File

@ -1,11 +1,12 @@
import React, { useEffect } from 'react';
import { SafeAreaView, View, TouchableOpacity } from 'react-native';
// Components
// import TabBar from './tabbar';
// Constants
import { useDispatch } from 'react-redux';
import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
import ROUTES from '../../../constants/routeNames';
// Styles
@ -13,40 +14,29 @@ import styles from './bottomTabBarStyles';
import Icon, { IconContainer } from '../../icon';
import scalePx from '../../../utils/scalePx';
import { updateActiveBottomTab } from '../../../redux/actions/uiAction';
import { useDispatch } from 'react-redux';
import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
const BottomTabBarView = ({
state : { routes , index },
state: { routes, index },
navigation,
descriptors
}:BottomTabBarProps) => {
descriptors,
}: BottomTabBarProps) => {
const dispatch = useDispatch();
useEffect(()=>{
dispatch(updateActiveBottomTab(routes[index].name))
},[index])
useEffect(() => {
dispatch(updateActiveBottomTab(routes[index].name));
}, [index]);
const _jumpTo = (route, isFocused) => {
if(route.name === ROUTES.TABBAR.POST_BUTTON){
navigation.navigate(ROUTES.SCREENS.EDITOR, {key: 'editor_post'})
if (route.name === ROUTES.TABBAR.POST_BUTTON) {
navigation.navigate(ROUTES.SCREENS.EDITOR, { key: 'editor_post' });
return;
}
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
})
});
//TODO: also enable tap to scroll up feature
if (!isFocused && !event.defaultPrevented) {
@ -54,48 +44,38 @@ const BottomTabBarView = ({
}
};
const _tabButtons = routes.map((route, idx) => {
const {tabBarActiveTintColor, tabBarInactiveTintColor} = descriptors[route.key].options
const { tabBarActiveTintColor, tabBarInactiveTintColor } = descriptors[route.key].options;
const isFocused = index == idx;
const iconColor = isFocused ? tabBarActiveTintColor : tabBarInactiveTintColor
const iconColor = isFocused ? tabBarActiveTintColor : tabBarInactiveTintColor;
let _iconProps = {
iconType:"MaterialIcons",
style:{ padding: 15 },
name:route.params.iconName,
color:iconColor,
size:scalePx(26),
}
iconType: 'MaterialIcons',
style: { padding: 15 },
name: route.params.iconName,
color: iconColor,
size: scalePx(26),
};
let _tabBarIcon = <Icon {..._iconProps}/>
let _tabBarIcon = <Icon {..._iconProps} />;
switch (route.name) {
case ROUTES.TABBAR.NOTIFICATION:
_tabBarIcon = (<IconContainer
isBadge badgeType="notification" {..._iconProps}/>)
_tabBarIcon = <IconContainer isBadge badgeType="notification" {..._iconProps} />;
break;
case ROUTES.TABBAR.POST_BUTTON:
_iconProps.iconType = "MaterialCommunityIcons"
_tabBarIcon = <Icon {..._iconProps} />
_iconProps.iconType = 'MaterialCommunityIcons';
_tabBarIcon = <Icon {..._iconProps} />;
break;
}
return (
<View key={route.key} style={{ flex: 1, alignItems: 'center' }}>
<TouchableOpacity onPress={() => _jumpTo(route, isFocused)}>
{_tabBarIcon}
</TouchableOpacity>
<TouchableOpacity onPress={() => _jumpTo(route, isFocused)}>{_tabBarIcon}</TouchableOpacity>
</View>
)
})
);
});
return (
<SafeAreaView style={styles.wrapper}>
{_tabButtons}
</SafeAreaView>
);
return <SafeAreaView style={styles.wrapper}>{_tabButtons}</SafeAreaView>;
};
export default BottomTabBarView;

View File

@ -4,6 +4,7 @@ import { useIntl } from 'react-intl';
import get from 'lodash/get';
import { View as AnimatedView } from 'react-native-animatable';
import { useDispatch } from 'react-redux';
import { getTimeFromNow } from '../../../utils/time';
// Constants
@ -18,7 +19,6 @@ import { TextWithIcon } from '../../basicUIElements';
import styles from './commentStyles';
import { useAppSelector } from '../../../hooks';
import { OptionsModal } from '../../atoms';
import { useDispatch } from 'react-redux';
import { showReplyModal } from '../../../redux/actions/uiAction';
import postTypes from '../../../constants/postTypes';
@ -42,14 +42,16 @@ const CommentView = ({
hideManyCommentsButton,
openReplyThread,
fetchedAt,
incrementRepliesCount
incrementRepliesCount,
}) => {
const intl = useIntl();
const dispatch = useDispatch();
const actionSheet = useRef(null);
const repliesContainerRef = useRef<AnimatedView>(null);
const isMuted = useAppSelector(state => state.account.currentAccount.mutes?.indexOf(comment.author) > -1);
const isMuted = useAppSelector(
(state) => state.account.currentAccount.mutes?.indexOf(comment.author) > -1,
);
const lastCacheUpdate = useAppSelector((state) => state.cache.lastUpdate);
const cachedComments = useAppSelector((state) => state.cache.comments);
@ -61,17 +63,16 @@ const CommentView = ({
const [childCount, setChildCount] = useState(comment.children);
const [replies, setReplies] = useState(comment.replies);
useEffect(()=>{
if(isShowSubComments){
setTimeout(()=>{
if(repliesContainerRef.current){
useEffect(() => {
if (isShowSubComments) {
setTimeout(() => {
if (repliesContainerRef.current) {
setIsShowSubComments(true);
repliesContainerRef.current.slideInRight(300);
}
},150)
}, 150);
}
},[])
}, []);
useEffect(() => {
if (comment) {
@ -79,7 +80,6 @@ const CommentView = ({
}
}, [comment]);
useEffect(() => {
const postPath = `${comment.author || ''}/${comment.permlink || ''}`;
//this conditional makes sure on targetted already fetched post is updated
@ -107,11 +107,10 @@ const CommentView = ({
}, [lastCacheUpdate]);
const _showSubCommentsToggle = (force) => {
if (((replies && replies.length > 0) || force)) {
if ((replies && replies.length > 0) || force) {
if (repliesContainerRef.current) {
if (_isShowSubComments) {
repliesContainerRef.current.slideOutRight(300).then(()=>{
repliesContainerRef.current.slideOutRight(300).then(() => {
setIsShowSubComments(false);
});
} else {
@ -119,233 +118,213 @@ const CommentView = ({
repliesContainerRef.current.slideInRight(300);
}
}
setIsPressedShowButton(true);
} else if (openReplyThread) {
openReplyThread();
}
};
const _handleCacheVoteIncrement = () => {
//fake increment vote using based on local change
setCacheVoteIcrement(1);
};
const _incrementRepliesCount = () => {
if (commentNumber > 1 && incrementRepliesCount) {
incrementRepliesCount();
}
setChildCount(childCount + 1);
}
const _handleOnReplyPress = () => {
if (isLoggedIn) {
dispatch(showReplyModal(comment));
} else {
console.log('Not LoggedIn');
}
}
const _renderReadMoreButton = () => (
<TextWithIcon
wrapperStyle={styles.rightButton}
textStyle={!isPressedShowButton && styles.moreText}
iconType="MaterialIcons"
isClickable
iconStyle={styles.iconStyle}
iconSize={16}
onPress={() => openReplyThread && openReplyThread()}
text={
!isPressedShowButton
? intl.formatMessage({ id: 'comments.read_more' })
: ''
} else if (openReplyThread) {
openReplyThread();
}
/>
};
)
const _handleCacheVoteIncrement = () => {
//fake increment vote using based on local change
setCacheVoteIcrement(1);
};
const _renderReplies = () => {
const _incrementRepliesCount = () => {
if (commentNumber > 1 && incrementRepliesCount) {
incrementRepliesCount();
}
setChildCount(childCount + 1);
};
const _handleOnReplyPress = () => {
if (isLoggedIn) {
dispatch(showReplyModal(comment));
} else {
console.log('Not LoggedIn');
}
};
return (
<AnimatedView ref={repliesContainerRef}>
{_isShowSubComments &&
<Comments
isShowComments={isShowComments}
commentNumber={commentNumber + 1}
marginLeft={20}
isShowSubComments={true}
avatarSize={avatarSize || 24}
author={comment.author}
permlink={comment.permlink}
commentCount={childCount}
comments={comment.replies}
hasManyComments={commentNumber === 5 && get(comment, 'children') > 0}
fetchPost={fetchPost}
hideManyCommentsButton={hideManyCommentsButton}
mainAuthor={mainAuthor}
fetchedAt={fetchedAt}
incrementRepliesCount={_incrementRepliesCount}
handleOnReplyPress={_handleOnReplyPress}
/>}
</AnimatedView>
const _renderReadMoreButton = () => (
<TextWithIcon
wrapperStyle={styles.rightButton}
textStyle={!isPressedShowButton && styles.moreText}
iconType="MaterialIcons"
isClickable
iconStyle={styles.iconStyle}
iconSize={16}
onPress={() => openReplyThread && openReplyThread()}
text={!isPressedShowButton ? intl.formatMessage({ id: 'comments.read_more' }) : ''}
/>
);
)
}
const _renderReplies = () => {
return (
<AnimatedView ref={repliesContainerRef}>
{_isShowSubComments && (
<Comments
isShowComments={isShowComments}
commentNumber={commentNumber + 1}
marginLeft={20}
isShowSubComments={true}
avatarSize={avatarSize || 24}
author={comment.author}
permlink={comment.permlink}
commentCount={childCount}
comments={comment.replies}
hasManyComments={commentNumber === 5 && get(comment, 'children') > 0}
fetchPost={fetchPost}
hideManyCommentsButton={hideManyCommentsButton}
mainAuthor={mainAuthor}
fetchedAt={fetchedAt}
incrementRepliesCount={_incrementRepliesCount}
handleOnReplyPress={_handleOnReplyPress}
/>
)}
</AnimatedView>
);
};
const _renderComment = () => {
return ((
<View style={[{ marginLeft: 2, marginTop: -6 }]}>
<CommentBody
commentDepth={comment.depth}
reputation={comment.author_reputation}
handleOnUserPress={handleOnUserPress}
handleOnLongPress={handleOnLongPress}
body={comment.body}
created={comment.created}
key={`key-${comment.permlink}`}
isMuted={isMuted}
/>
<Fragment>
<View style={styles.footerWrapper}>
{_renderActionPanel()}
</View>
{commentNumber > 1 &&
childCount > 0 &&
!replies?.length &&
_renderReadMoreButton()
}
</Fragment>
</View>
))
}
const _renderActionPanel = () => {
return (
<>
<Upvote
activeVotes={activeVotes}
isShowPayoutValue
content={comment}
handleCacheVoteIncrement={_handleCacheVoteIncrement}
parentType={postTypes.COMMENT}
/>
<TextWithIcon
iconName="heart-outline"
iconSize={20}
wrapperStyle={styles.leftButton}
iconType="MaterialCommunityIcons"
isClickable
onPress={() =>
handleOnVotersPress &&
activeVotes.length > 0 &&
handleOnVotersPress(activeVotes, comment)
}
text={activeVotes.length + cacheVoteIcrement}
textStyle={styles.voteCountText}
/>
{isLoggedIn && (
<IconButton
size={20}
iconStyle={styles.leftIcon}
style={styles.leftButton}
name="comment-outline"
onPress={_handleOnReplyPress}
iconType="MaterialCommunityIcons"
const _renderComment = () => {
return (
<View style={[{ marginLeft: 2, marginTop: -6 }]}>
<CommentBody
commentDepth={comment.depth}
reputation={comment.author_reputation}
handleOnUserPress={handleOnUserPress}
handleOnLongPress={handleOnLongPress}
body={comment.body}
created={comment.created}
key={`key-${comment.permlink}`}
isMuted={isMuted}
/>
)}
{currentAccountUsername === comment.author && (
<Fragment>
<View style={styles.footerWrapper}>{_renderActionPanel()}</View>
{commentNumber > 1 && childCount > 0 && !replies?.length && _renderReadMoreButton()}
</Fragment>
</View>
);
};
const _renderActionPanel = () => {
return (
<>
<Upvote
activeVotes={activeVotes}
isShowPayoutValue
content={comment}
handleCacheVoteIncrement={_handleCacheVoteIncrement}
parentType={postTypes.COMMENT}
/>
<TextWithIcon
iconName="heart-outline"
iconSize={20}
wrapperStyle={styles.leftButton}
iconType="MaterialCommunityIcons"
isClickable
onPress={() =>
handleOnVotersPress &&
activeVotes.length > 0 &&
handleOnVotersPress(activeVotes, comment)
}
text={activeVotes.length + cacheVoteIcrement}
textStyle={styles.voteCountText}
/>
{isLoggedIn && (
<IconButton
size={20}
iconStyle={styles.leftIcon}
style={styles.leftButton}
name="create"
onPress={() => handleOnEditPress && handleOnEditPress(comment)}
iconType="MaterialIcons"
name="comment-outline"
onPress={_handleOnReplyPress}
iconType="MaterialCommunityIcons"
/>
{!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>
)}
</Fragment>
)}
)}
{currentAccountUsername === comment.author && (
<Fragment>
<IconButton
size={20}
iconStyle={styles.leftIcon}
style={styles.leftButton}
name="create"
onPress={() => handleOnEditPress && handleOnEditPress(comment)}
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>
)}
</Fragment>
)}
{commentNumber === 1 && childCount > 0 && (
<View style={styles.rightButtonWrapper}>
<TextWithIcon
wrapperStyle={styles.rightButton}
iconName={_isShowSubComments ? 'keyboard-arrow-up' : 'keyboard-arrow-down'}
textStyle={styles.moreText}
iconType="MaterialIcons"
isClickable
iconStyle={styles.iconStyle}
iconSize={16}
onPress={() => _showSubCommentsToggle()}
text={`${childCount} ${intl.formatMessage({ id: 'comments.more_replies' })}`}
/>
</View>
)}
{commentNumber === 1 && childCount > 0 && (
<View style={styles.rightButtonWrapper}>
<TextWithIcon
wrapperStyle={styles.rightButton}
iconName={_isShowSubComments ? 'keyboard-arrow-up' : 'keyboard-arrow-down'}
textStyle={styles.moreText}
iconType="MaterialIcons"
isClickable
iconStyle={styles.iconStyle}
iconSize={16}
onPress={() => _showSubCommentsToggle()}
text={`${childCount} ${intl.formatMessage({ id: 'comments.more_replies' })}`}
/>
</View>
)}
</>
);
};
</>
)
}
const customContainerStyle = commentNumber > 2 ? { marginLeft: 44 } : null;
const customContainerStyle = commentNumber > 2 ? { marginLeft: 44 } : null
return (
<Fragment>
<View style={{ ...styles.commentContainer, ...customContainerStyle }}>
<PostHeaderDescription
key={comment.permlink}
date={getTimeFromNow(comment.created)}
name={comment.author}
reputation={comment.author_reputation}
size={avatarSize || 40}
currentAccountUsername={currentAccountUsername}
isShowOwnerIndicator={mainAuthor === comment.author}
isHideImage={isHideImage}
inlineTime={true}
customStyle={{ alignItems: 'flex-start', paddingLeft: 12 }}
showDotMenuButton={true}
handleOnDotPress={handleOnLongPress}
secondaryContentComponent={_renderComment()}
/>
return (
<Fragment>
<View style={{ ...styles.commentContainer, ...customContainerStyle }}>
<PostHeaderDescription
key={comment.permlink}
date={getTimeFromNow(comment.created)}
name={comment.author}
reputation={comment.author_reputation}
size={avatarSize || 40}
currentAccountUsername={currentAccountUsername}
isShowOwnerIndicator={mainAuthor === comment.author}
isHideImage={isHideImage}
inlineTime={true}
customStyle={{ alignItems: 'flex-start', paddingLeft: 12 }}
showDotMenuButton={true}
handleOnDotPress={handleOnLongPress}
secondaryContentComponent={_renderComment()}
/>
{commentNumber > 0 && _renderReplies()}
</View>
</Fragment>
);
{commentNumber > 0 && _renderReplies()}
</View>
</Fragment>
);
};
export default CommentView;

View File

@ -4,14 +4,13 @@ import get from 'lodash/get';
import { useIntl } from 'react-intl';
// Components
import EStyleSheet from 'react-native-extended-stylesheet';
import { Comment, TextButton } from '../..';
// Styles
import styles from './commentStyles';
import EStyleSheet from 'react-native-extended-stylesheet';
import { OptionsModal } from '../../atoms';
const CommentsView = ({
avatarSize,
commentCount,
@ -36,13 +35,12 @@ const CommentsView = ({
flatListProps,
openReplyThread,
fetchedAt,
incrementRepliesCount
incrementRepliesCount,
}) => {
const [selectedComment, setSelectedComment] = useState(null);
const intl = useIntl();
const commentMenu = useRef<any>();
const _openCommentMenu = (item) => {
if (commentMenu.current) {
setSelectedComment(item);
@ -52,21 +50,20 @@ const CommentsView = ({
const _openReplyThread = (item) => {
if (item && openReplyThread) {
openReplyThread(item)
openReplyThread(item);
}
}
};
const _readMoreComments = () => {
if (comments[0] && openReplyThread) {
openReplyThread(comments[0])
openReplyThread(comments[0]);
}
};
const _onMenuItemPress = (index) => {
handleOnPressCommentMenu(index, selectedComment)
handleOnPressCommentMenu(index, selectedComment);
setSelectedComment(null);
}
};
const menuItems = [
intl.formatMessage({ id: 'post.copy_link' }),
@ -75,7 +72,6 @@ const CommentsView = ({
intl.formatMessage({ id: 'alert.cancel' }),
];
if (!hideManyCommentsButton && hasManyComments) {
return (
<TextButton
@ -87,7 +83,6 @@ const CommentsView = ({
);
}
const _renderItem = ({ item }) => {
return (
<Comment
@ -115,29 +110,30 @@ const CommentsView = ({
fetchedAt={fetchedAt}
incrementRepliesCount={incrementRepliesCount}
/>
)
);
};
const styleOerride = commentNumber > 1 ? {
backgroundColor: EStyleSheet.value('$primaryLightBackground'),
marginTop: 8,
} : null
const styleOerride =
commentNumber > 1
? {
backgroundColor: EStyleSheet.value('$primaryLightBackground'),
marginTop: 8,
}
: null;
const _renderEmptyContent = () => {
if(commentNumber > 1){
if (commentNumber > 1) {
return;
}
const _onPress = () => {
handleOnReplyPress()
}
handleOnReplyPress();
};
return (
<Text onPress={_onPress} style={styles.emptyText}>
{intl.formatMessage({ id: "comments.no_comments" })}
{intl.formatMessage({ id: 'comments.no_comments' })}
</Text>
)
}
);
};
return (
<Fragment>

View File

@ -1,57 +1,56 @@
import { View, Text } from 'react-native'
import React, { forwardRef, useImperativeHandle, useRef } from 'react'
import UserAvatar from '../../userAvatar';
import { View, Text } 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 './WriteCommentButtonStyles';
import { useAppSelector } from '../../../hooks';
import showLoginAlert from '../../../utils/showLoginAlert';
import { useIntl } from 'react-intl';
interface WriteCommentButton {
onPress: () => void;
onPress: () => void;
}
export const WriteCommentButton = forwardRef(({ onPress }, ref) => {
const intl = useIntl();
const intl = useIntl();
const animatedContainer = useRef<AnimatedView>();
const animatedContainer = useRef<AnimatedView>();
const isLoggedIn = useAppSelector(state => state.application.isLoggedIn);
const currentAccount = useAppSelector(state => state.account.currentAccount);
const isLoggedIn = useAppSelector((state) => state.application.isLoggedIn);
const currentAccount = useAppSelector((state) => state.account.currentAccount);
useImperativeHandle(ref, () => ({
bounce: () => {
console.log("bouncing")
if (animatedContainer.current) {
animatedContainer.current.swing(1000);
}
},
}));
useImperativeHandle(ref, () => ({
bounce: () => {
console.log('bouncing');
if (animatedContainer.current) {
animatedContainer.current.swing(1000);
}
},
}));
const _onPress = () => {
if (!isLoggedIn) {
showLoginAlert({ intl })
return;
}
if (onPress) {
onPress();
}
const _onPress = () => {
if (!isLoggedIn) {
showLoginAlert({ intl });
return;
}
if (onPress) {
onPress();
}
};
return (
<AnimatedView ref={animatedContainer}>
<TouchableOpacity onPress={_onPress}>
<View style={styles.container}>
<UserAvatar username={currentAccount.username} />
<View style={styles.inputContainer}>
<Text style={styles.inputPlaceholder}>
{intl.formatMessage({id:'quick_reply.placeholder'})}
</Text>
</View>
</View>
</TouchableOpacity>
</AnimatedView>
)
})
return (
<AnimatedView ref={animatedContainer}>
<TouchableOpacity onPress={_onPress}>
<View style={styles.container}>
<UserAvatar username={currentAccount.username} />
<View style={styles.inputContainer}>
<Text style={styles.inputPlaceholder}>
{intl.formatMessage({ id: 'quick_reply.placeholder' })}
</Text>
</View>
</View>
</TouchableOpacity>
</AnimatedView>
);
});

View File

@ -3,91 +3,85 @@ import EStyleSheet from 'react-native-extended-stylesheet';
import getWindowDimensions from '../../utils/getWindowDimensions';
export default EStyleSheet.create({
modalStyle: {
backgroundColor: '$primaryBackgroundColor',
margin:0,
paddingTop:32,
paddingBottom:8,
},
modalStyle: {
backgroundColor: '$primaryBackgroundColor',
margin: 0,
paddingTop: 32,
paddingBottom: 8,
},
sheetContent: {
backgroundColor: '$primaryBackgroundColor',
},
sheetContent: {
backgroundColor: '$primaryBackgroundColor',
},
container:{
marginTop:16,
marginBottom:44,
paddingHorizontal:24,
alignItems:'center',
justifyContent:'space-between',
} as ViewStyle,
container: {
marginTop: 16,
marginBottom: 44,
paddingHorizontal: 24,
alignItems: 'center',
justifyContent: 'space-between',
} as ViewStyle,
imageStyle:{
marginTop:8,
height:150,
width:150,
} as ImageStyle,
imageStyle: {
marginTop: 8,
height: 150,
width: 150,
} as ImageStyle,
textContainer:{
marginTop:32,
marginBottom:44,
} as ViewStyle,
textContainer: {
marginTop: 32,
marginBottom: 44,
} as ViewStyle,
title: {
color: '$primaryBlack',
alignSelf: 'center',
textAlign: 'center',
fontSize: 20,
fontWeight: '800',
} as TextStyle,
title: {
color: '$primaryBlack',
alignSelf: 'center',
textAlign: 'center',
fontSize: 20,
fontWeight: '800',
} as TextStyle,
bodyText: {
color: '$primaryBlack',
alignSelf: 'center',
textAlign: 'center',
fontSize: 16,
fontWeight: '600',
marginTop:4,
} as TextStyle,
bodyText: {
color: '$primaryBlack',
alignSelf: 'center',
textAlign: 'center',
fontSize: 16,
fontWeight: '600',
marginTop: 4,
} as TextStyle,
btnText:{
color:'$pureWhite'
} as TextStyle,
btnText: {
color: '$pureWhite',
} as TextStyle,
button:{
button: {
backgroundColor: '$primaryBlue',
width: 150,
paddingVertical: 16,
borderRadius: 32,
justifyContent: 'center',
alignItems: 'center',
} as ViewStyle,
backgroundColor:'$primaryBlue',
width:150,
paddingVertical:16,
borderRadius:32,
justifyContent:'center',
alignItems:'center'
} as ViewStyle,
actionPanel: {
width: '100%',
flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center',
} as ViewStyle,
checkView: {
width: getWindowDimensions().width - 80,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginHorizontal: 20,
marginVertical: 4,
} as ViewStyle,
actionPanel:{
width:'100%',
flexDirection:'row',
justifyContent:'space-around',
alignItems:'center',
} as ViewStyle,
checkView: {
width:getWindowDimensions().width - 80,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems:'center',
marginHorizontal: 20,
marginVertical:4,
} as ViewStyle,
informationText: {
color: '$primaryBlack',
margin: 10,
fontSize:18,
} as TextStyle,
})
informationText: {
color: '$primaryBlack',
margin: 10,
fontSize: 18,
} as TextStyle,
});

View File

@ -1,5 +1,5 @@
import React from 'react';
import { View, Text, ActivityIndicator, TouchableHighlight} from 'react-native';
import { View, Text, ActivityIndicator, TouchableHighlight } from 'react-native';
import EStyleSheet from 'react-native-extended-stylesheet';
// External components
import ModalDropdown from 'react-native-modal-dropdown';
@ -25,28 +25,28 @@ const renderDropdownRow = (
noHighlight,
dropdownRowWrapper,
) => (
<View
<View
style={[
styles.dropdownRow,
dropdownRowWrapper,
!noHighlight && highlighted && styles.highlightedRow,
]}
>
<Text
style={[
styles.dropdownRow,
dropdownRowWrapper,
!noHighlight && highlighted && styles.highlightedRow,
rowTextStyle || styles.rowText,
!noHighlight && highlighted && styles.highlightedRowText,
]}
>
<Text
style={[
rowTextStyle || styles.rowText,
!noHighlight && highlighted && styles.highlightedRowText,
]}
>
{rowData}
</Text>
</View>
{rowData}
</Text>
</View>
);
const adjustDropdownFrame = (style:any) => {
style.left = 'auto'
style.right = 10
return style
}
const adjustDropdownFrame = (style: any) => {
style.left = 'auto';
style.right = 10;
return style;
};
const DropdownButtonView = ({
childIconWrapperStyle,
children,
@ -72,7 +72,10 @@ const DropdownButtonView = ({
<ModalDropdown
ref={dropdownRef}
renderRowComponent={TouchableHighlight}
renderRowProps={{ underlayColor: EStyleSheet.value('$modalBackground'), style:styles.rowWrapper}}
renderRowProps={{
underlayColor: EStyleSheet.value('$modalBackground'),
style: styles.rowWrapper,
}}
style={[!style ? styles.button : style]}
textStyle={[textStyle || styles.buttonText]}
dropdownStyle={[styles.dropdown, dropdownStyle, { height: 32 * (options.length + 1.5) }]}
@ -94,13 +97,11 @@ const DropdownButtonView = ({
dropdownRowWrapper,
)
}
adjustFrame={(style: any) => adjustDropdownFrame(style) }
adjustFrame={(style: any) => adjustDropdownFrame(style)}
>
{isHasChildIcon && !isLoading ? (
<View style={styles.childrenWrapper}>
<Text style={[textStyle || styles.buttonText]}>
{defaultText}
</Text>
<Text style={[textStyle || styles.buttonText]}>{defaultText}</Text>
<View style={[styles.iconWrapper, childIconWrapperStyle && childIconWrapperStyle]}>
<Icon
style={[styles.dropdownIcon, iconStyle]}

View File

@ -16,8 +16,7 @@ import { Tag } from '../../../basicUIElements';
import { isCommunity } from '../../../../utils/communityValidation';
import { toastNotification } from '../../../../redux/actions/uiAction';
const SEPARATOR_REGEX = /[,\s]/
const SEPARATOR_REGEX = /[,\s]/;
const TagInput = ({ value, handleTagChanged, intl, isPreviewActive, autoFocus, setCommunity }) => {
const dispatch = useAppDispatch();

View File

@ -3,6 +3,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import { View as AnimatedView } from 'react-native-animatable';
import { useDispatch } from 'react-redux';
import { useIntl } from 'react-intl';
import { IconButton } from '..';
import { toastNotification } from '../../redux/actions/uiAction';
import UserAvatar from '../userAvatar';
@ -11,7 +12,6 @@ import ROUTES from '../../constants/routeNames';
// Styles
import styles, { CONTAINER_HEIGHT } from './styles';
import { navigate } from '../../navigation/service';
import { useIntl } from 'react-intl';
interface RemoteMessage {
data: {
@ -26,15 +26,13 @@ interface RemoteMessage {
notification: {
body: string;
title: string;
}
};
}
interface Props {
remoteMessage: RemoteMessage
remoteMessage: RemoteMessage;
}
const ForegroundNotification = ({ remoteMessage }: Props) => {
const intl = useIntl();
@ -48,26 +46,23 @@ const ForegroundNotification = ({ remoteMessage }: Props) => {
const [title, setTitle] = useState('');
const [body, setBody] = useState('');
useEffect(() => {
if (remoteMessage) {
const { source, target, type, id } = remoteMessage.data;
if (activeId !== id && (type === 'reply' || type === 'mention')) {
let titlePrefixId = '';
switch (type) {
case 'reply':
titlePrefixId = 'notification.reply_on'
titlePrefixId = 'notification.reply_on';
break;
case 'mention':
titlePrefixId = 'notification.mention_on'
titlePrefixId = 'notification.mention_on';
break;
}
setActiveId(id);
setUsername(source);
setTitle(`${intl.formatMessage({ id: titlePrefixId })} @${target}`)
setTitle(`${intl.formatMessage({ id: titlePrefixId })} @${target}`);
setBody(intl.formatMessage({ id: 'notification.reply_body' }));
show();
}
@ -77,14 +72,14 @@ const ForegroundNotification = ({ remoteMessage }: Props) => {
if (hideTimeoutRef.current) {
clearTimeout(hideTimeoutRef.current);
}
}
};
}, [remoteMessage]);
const show = () => {
setIsVisible(true)
setIsVisible(true);
hideTimeoutRef.current = setTimeout(() => {
hide();
}, duration)
}, duration);
};
@ -93,14 +88,12 @@ const ForegroundNotification = ({ remoteMessage }: Props) => {
await containerRef.current.fadeOutUp(300);
setIsVisible(false);
if(hideTimeoutRef.current){
if (hideTimeoutRef.current){
clearTimeout(hideTimeoutRef.current);
}
}
};
const _onPress = () => {
const { data } = remoteMessage;
const fullPermlink =
@ -110,7 +103,7 @@ const ForegroundNotification = ({ remoteMessage }: Props) => {
author: get(data, 'source', ''),
permlink: fullPermlink,
};
let key = fullPermlink
let key = fullPermlink;
let routeName = ROUTES.SCREENS.POST;
navigate({
@ -119,41 +112,38 @@ const ForegroundNotification = ({ remoteMessage }: Props) => {
key,
});
hide();
}
};
return (
isVisible &&
<AnimatedView
ref={containerRef}
style={styles.container}
animation='slideInDown'
duration={500}>
isVisible && (
<AnimatedView
ref={containerRef}
style={styles.container}
animation="slideInDown"
duration={500}
>
<View style={styles.contentContainer}>
<TouchableOpacity onPress={_onPress} style={{ flexShrink: 1 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', marginRight: 24 }}>
<UserAvatar username={username} />
<View style={styles.contentContainer}>
<TouchableOpacity onPress={_onPress} style={{ flexShrink: 1 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', marginRight: 24 }}>
<UserAvatar username={username} />
<View style={{ flexShrink: 1 }}>
<Text style={styles.text} numberOfLines={1}>{title}</Text>
<Text style={styles.text} numberOfLines={1}>{body}</Text>
<View style={{ flexShrink: 1 }}>
<Text style={styles.text} numberOfLines={1}>
{title}
</Text>
<Text style={styles.text} numberOfLines={1}>
{body}
</Text>
</View>
</View>
</View>
</TouchableOpacity>
<IconButton
name='close'
color="white"
size={28}
onPress={hide}
/>
</View>
</AnimatedView>
)
}
</TouchableOpacity>
<IconButton name="close" color="white" size={28} onPress={hide} />
</View>
</AnimatedView>
);
);
};
export default ForegroundNotification;

View File

@ -5,16 +5,16 @@ import { getStatusBarHeight } from 'react-native-iphone-x-helper';
export const CONTAINER_HEIGHT = getStatusBarHeight() + 100;
export default EStyleSheet.create({
container:{
container: {
position: 'absolute',
top:0,
justifyContent:'center',
top: 0,
justifyContent: 'center',
zIndex: 9999,
marginHorizontal:8,
paddingTop:16,
marginHorizontal: 8,
paddingTop: 16,
marginTop: Platform.select({
ios:getStatusBarHeight() + 12,
android:8,
ios: getStatusBarHeight() + 12,
android: 8,
}),
backgroundColor: '$darkGrayBackground',
shadowColor: '#5f5f5fbf',
@ -23,20 +23,20 @@ export default EStyleSheet.create({
height: 5,
},
elevation: 3,
borderRadius:12,
borderRadius: 12,
width: '$deviceWidth - 16',
},
contentContainer:{
flexDirection:'row',
justifyContent:'space-between',
alignItems:'center',
paddingBottom:16,
paddingHorizontal:16
contentContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingBottom: 16,
paddingHorizontal: 16,
},
text: {
color: 'white',
fontWeight: 'bold',
fontSize: 14,
paddingLeft:16,
paddingLeft: 16,
},
});

View File

@ -13,21 +13,20 @@ import { getResizedAvatar } from '../../../utils/image';
// Styles
import styles from './formInputStyles';
interface Props extends TextInputProps {
type:string;
isFirstImage:boolean;
isEditable?:boolean;
leftIconName?:string;
rightIconName?:string;
iconType?:string;
wrapperStyle:ViewStyle;
height:number;
inputStyle:TextStyle;
isValid:boolean;
onChange?:(value:string)=>void;
onFocus?:()=>void;
onBlur?:()=>void;
type: string;
isFirstImage: boolean;
isEditable?: boolean;
leftIconName?: string;
rightIconName?: string;
iconType?: string;
wrapperStyle: ViewStyle;
height: number;
inputStyle: TextStyle;
isValid: boolean;
onChange?: (value: string) => void;
onFocus?: () => void;
onBlur?: () => void;
}
const FormInputView = ({
@ -48,7 +47,7 @@ const FormInputView = ({
onBlur,
onFocus,
...props
}:Props) => {
}: Props) => {
const [_value, setValue] = useState(value || '');
const [inputBorderColor, setInputBorderColor] = useState('#e7e7e7');
const [_isValid, setIsValid] = useState(true);
@ -65,7 +64,7 @@ const FormInputView = ({
const _handleOnFocus = () => {
setInputBorderColor('#357ce6');
if(onFocus){
if (onFocus) {
onFocus();
}
};

View File

@ -46,13 +46,8 @@ const HeaderView = ({
});
};
const _renderAvatar = () => (
<TouchableOpacity
style={styles.avatarWrapper}
onPress={handleOpenDrawer}
disabled={isReverse}
>
<TouchableOpacity style={styles.avatarWrapper} onPress={handleOpenDrawer} disabled={isReverse}>
<LinearGradient
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
@ -69,14 +64,17 @@ const HeaderView = ({
/>
</LinearGradient>
</TouchableOpacity>
)
);
const _renderTitle = () => (
<>
{displayName || username ? (
<View style={[styles.titleWrapper, isReverse && styles.titleWrapperReverse]}>
{displayName && <Text numberOfLines={1} style={styles.title}>{displayName}</Text>}
{displayName && (
<Text numberOfLines={1} style={styles.title}>
{displayName}
</Text>
)}
<Text style={styles.subTitle}>
{`@${username}`}
{reputation && ` (${reputation})`}
@ -94,8 +92,7 @@ const HeaderView = ({
</View>
)}
</>
)
);
const _renderActionButtons = () => (
<>
@ -123,11 +120,10 @@ const HeaderView = ({
</View>
)}
</>
)
);
return (
<SafeAreaView style={[styles.container, isReverse && styles.containerReverse]}>
{!hideUser && (
<>
<SearchModal

View File

@ -1,21 +1,15 @@
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { useIntl } from 'react-intl';
import {
Platform,
Text,
TouchableOpacity,
View,
ActivityIndicator,
} from 'react-native';
import { Platform, Text, TouchableOpacity, View, ActivityIndicator } from 'react-native';
import { renderPostBody } from '@ecency/render-helper';
import { ScrollView } from 'react-native-gesture-handler';
import Clipboard from '@react-native-clipboard/clipboard';
import { MainButton, PostBody, TextButton } from '..';
import styles from './insertLinkModalStyles';
import TextInput from '../textInput';
import { delay } from '../../utils/editor';
import { isStringWebLink } from '../markdownEditor/children/formats/utils';
import { renderPostBody } from '@ecency/render-helper';
import { ScrollView } from 'react-native-gesture-handler';
import applyWebLinkFormat from '../markdownEditor/children/formats/applyWebLinkFormat';
import Clipboard from '@react-native-clipboard/clipboard';
import getWindowDimensions from '../../utils/getWindowDimensions';
import Modal from '../modal';
@ -49,7 +43,6 @@ export const InsertLinkModal = forwardRef(
const labelInputRef = useRef(null);
const urlInputRef = useRef(null);
useImperativeHandle(ref, () => ({
showModal: async ({ selectedText, selection }) => {
if (selectedText) {
@ -141,8 +134,8 @@ export const InsertLinkModal = forwardRef(
<View style={styles.floatingContainer}>
<TextButton
style={styles.cancelButton}
onPress={() => setVisible(false)}// sheetModalRef.current?.setModalVisible(false)}
text={'Cancel'}
onPress={() => setVisible(false)} // sheetModalRef.current?.setModalVisible(false)}
text="Cancel"
/>
<MainButton
style={styles.insertBtn}
@ -277,13 +270,13 @@ export const InsertLinkModal = forwardRef(
) : null}
</View>
</ScrollView>
{isLoading && <ActivityIndicator color={'$primaryBlue'} />}
{isLoading && <ActivityIndicator color="$primaryBlue" />}
</View>
</>
);
};
const _renderContent = (
<ScrollView style={styles.container} keyboardShouldPersistTaps={'handled'}>
<ScrollView style={styles.container} keyboardShouldPersistTaps="handled">
{_renderInputs()}
{_renderPreview()}
{_renderFloatingPanel()}
@ -291,7 +284,6 @@ export const InsertLinkModal = forwardRef(
);
return (
<Modal
isOpen={visible}
handleOnModalClose={_handleOnCloseSheet}
@ -302,7 +294,6 @@ export const InsertLinkModal = forwardRef(
>
{_renderContent}
</Modal>
);
},
);

View File

@ -19,7 +19,7 @@ export default EStyleSheet.create({
paddingTop: 32,
paddingBottom: 16,
},
container: {
paddingVertical: 8,
backgroundColor: '$primaryBackgroundColor',

View File

@ -1,26 +1,31 @@
import { Keyboard, View, ViewStyle } from 'react-native'
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { IconButton, UploadsGalleryModal } from '../..'
import { FlatList, HandlerStateChangeEvent, PanGestureHandler, PanGestureHandlerEventPayload } from 'react-native-gesture-handler';
import { Keyboard, View, ViewStyle } from 'react-native';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import {
FlatList,
HandlerStateChangeEvent,
PanGestureHandler,
PanGestureHandlerEventPayload,
} from 'react-native-gesture-handler';
import { getBottomSpace } from 'react-native-iphone-x-helper';
import Animated, { Easing, Extrapolate } from 'react-native-reanimated';
import { IconButton, UploadsGalleryModal } from '../..';
import styles from '../styles/editorToolbarStyles';
import { useAppSelector } from '../../../hooks';
import { MediaInsertData } from '../../uploadsGalleryModal/container/uploadsGalleryModal';
import Formats from './formats/formats';
import { getBottomSpace } from 'react-native-iphone-x-helper';
import Animated, { Easing, Extrapolate } from 'react-native-reanimated';
type Props = {
insertedMediaUrls: string[],
paramFiles: any[]
isEditing: boolean,
isPreviewActive: boolean,
insertedMediaUrls: string[];
paramFiles: any[];
isEditing: boolean;
isPreviewActive: boolean;
setIsUploading: (isUploading: boolean) => void;
handleMediaInsert: (data: MediaInsertData[]) => void;
handleOnAddLinkPress: () => void;
handleOnClearPress: () => void;
handleOnMarkupButtonPress: (item) => void;
handleShowSnippets: () => void;
}
};
export const EditorToolbar = ({
insertedMediaUrls,
@ -32,11 +37,9 @@ export const EditorToolbar = ({
handleOnAddLinkPress,
handleOnClearPress,
handleOnMarkupButtonPress,
handleShowSnippets
handleShowSnippets,
}: Props) => {
const currentAccount = useAppSelector(state => state.account.currentAccount)
const currentAccount = useAppSelector((state) => state.account.currentAccount);
const uploadsGalleryModalRef = useRef<typeof UploadsGalleryModal>(null);
const translateY = useRef(new Animated.Value(200));
const shouldHideExtension = useRef(false);
@ -47,18 +50,12 @@ export const EditorToolbar = ({
const [isKeyboardVisible, setKeyboardVisible] = useState(false);
useEffect(() => {
const keyboardDidShowListener = Keyboard.addListener(
'keyboardDidShow',
() => {
setKeyboardVisible(true); // or some other action
}
);
const keyboardDidHideListener = Keyboard.addListener(
'keyboardDidHide',
() => {
setKeyboardVisible(false); // or some other action
}
);
const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => {
setKeyboardVisible(true); // or some other action
});
const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => {
setKeyboardVisible(false); // or some other action
});
return () => {
keyboardDidHideListener.remove();
@ -74,7 +71,9 @@ export const EditorToolbar = ({
iconStyle={styles.icon}
iconType={item.iconType}
name={item.icon}
onPress={() => { handleOnMarkupButtonPress && handleOnMarkupButtonPress(item) }}
onPress={() => {
handleOnMarkupButtonPress && handleOnMarkupButtonPress(item);
}}
/>
</View>
);
@ -86,8 +85,7 @@ export const EditorToolbar = ({
uploadsGalleryModalRef.current.toggleModal(true);
_revealExtension();
}
}
};
//handles extension closing
const _onGestureEvent = Animated.event(
@ -99,30 +97,38 @@ export const EditorToolbar = ({
},
],
{
useNativeDriver: false
}
useNativeDriver: false,
},
);
const consY = useMemo(() => translateY.current.interpolate({
inputRange: [0, 500],
outputRange: [0, 500],
extrapolate: Extrapolate.CLAMP
}), [translateY.current]);
const consY = useMemo(
() =>
translateY.current.interpolate({
inputRange: [0, 500],
outputRange: [0, 500],
extrapolate: Extrapolate.CLAMP,
}),
[translateY.current],
);
const _animatedStyle = {
transform: [
{
translateY: consY,
},
]
}
],
};
const _onPanHandlerStateChange = (e: HandlerStateChangeEvent<PanGestureHandlerEventPayload>) => {
console.log("handler state change", e.nativeEvent.velocityY, e.nativeEvent.velocityY > 300, e.nativeEvent.translationY);
shouldHideExtension.current = e.nativeEvent.velocityY > 300 || e.nativeEvent.translationY > (extensionHeight.current / 2);
}
console.log(
'handler state change',
e.nativeEvent.velocityY,
e.nativeEvent.velocityY > 300,
e.nativeEvent.translationY,
);
shouldHideExtension.current =
e.nativeEvent.velocityY > 300 || e.nativeEvent.translationY > extensionHeight.current / 2;
};
const _revealExtension = () => {
if (!isExtensionVisible) {
@ -135,9 +141,8 @@ export const EditorToolbar = ({
duration: 200,
toValue: 0,
easing: Easing.inOut(Easing.ease),
}).start()
}
}).start();
};
const _hideExtension = () => {
Animated.timing(translateY.current, {
@ -151,29 +156,31 @@ export const EditorToolbar = ({
uploadsGalleryModalRef.current.toggleModal(false);
}
});
}
};
const _onPanEnded = () => {
if (shouldHideExtension.current) {
_hideExtension()
_hideExtension();
} else {
_revealExtension();
}
}
};
const _renderExtension = () => {
return (
<PanGestureHandler onGestureEvent={_onGestureEvent}
<PanGestureHandler
onGestureEvent={_onGestureEvent}
onHandlerStateChange={_onPanHandlerStateChange}
onEnded={_onPanEnded}>
<Animated.View style={_animatedStyle}>
<View onLayout={(e) => {
extensionHeight.current = e.nativeEvent.layout.height;
console.log('extension height', extensionHeight.current)
}} style={styles.dropShadow}>
onEnded={_onPanEnded}
>
<Animated.View style={_animatedStyle}>
<View
onLayout={(e) => {
extensionHeight.current = e.nativeEvent.layout.height;
console.log('extension height', extensionHeight.current);
}}
style={styles.dropShadow}
>
{isExtensionVisible && <View style={styles.indicator} />}
<UploadsGalleryModal
ref={uploadsGalleryModalRef}
@ -184,23 +191,25 @@ export const EditorToolbar = ({
username={currentAccount.username}
hideToolbarExtension={_hideExtension}
handleMediaInsert={handleMediaInsert}
setIsUploading={setIsUploading} />
setIsUploading={setIsUploading}
/>
</View>
</Animated.View>
</PanGestureHandler>
)
}
);
};
const _containerStyle: ViewStyle = isExtensionVisible ? styles.container : styles.shadowedContainer;
const _containerStyle: ViewStyle = isExtensionVisible
? styles.container
: styles.shadowedContainer;
const _buttonsContainerStyle: ViewStyle = {
...styles.buttonsContainer,
borderTopWidth: isExtensionVisible ? 1 : 0,
paddingBottom: !isKeyboardVisible ? getBottomSpace() : 0
}
paddingBottom: !isKeyboardVisible ? getBottomSpace() : 0,
};
return (
<View style={_containerStyle}>
{_renderExtension()}
{!isPreviewActive && (
@ -220,10 +229,14 @@ export const EditorToolbar = ({
iconStyle={styles.icon}
iconType="FontAwesome"
name="link"
onPress={() => { handleOnAddLinkPress && handleOnAddLinkPress() }}
onPress={() => {
handleOnAddLinkPress && handleOnAddLinkPress();
}}
/>
<IconButton
onPress={() => { handleShowSnippets && handleShowSnippets() }}
onPress={() => {
handleShowSnippets && handleShowSnippets();
}}
style={styles.rightIcons}
size={20}
iconStyle={styles.icon}
@ -240,7 +253,9 @@ export const EditorToolbar = ({
/>
<View style={styles.clearButtonWrapper}>
<IconButton
onPress={() => { handleOnClearPress && handleOnClearPress() }}
onPress={() => {
handleOnClearPress && handleOnClearPress();
}}
size={20}
iconStyle={styles.clearIcon}
iconType="FontAwesome"
@ -251,9 +266,6 @@ export const EditorToolbar = ({
</View>
</View>
)}
</View>
)
}
);
};

View File

@ -1,108 +1,102 @@
import { MediaInsertData, MediaInsertStatus } from '../../../uploadsGalleryModal/container/uploadsGalleryModal';
import {
MediaInsertData,
MediaInsertStatus,
} from '../../../uploadsGalleryModal/container/uploadsGalleryModal';
import { replaceBetween } from './utils';
interface Selection {
start: number,
end: number
start: number;
end: number;
}
interface Args {
text: string;
selection: Selection;
setTextAndSelection: ({ selection: Selection, text: string }) => void,
items: MediaInsertData[]
text: string;
selection: Selection;
setTextAndSelection: ({ selection: Selection, text: string }) => void;
items: MediaInsertData[];
}
export default async ({ text, selection, setTextAndSelection, items }: Args) => {
//TODO: check if placeholder already present in text body
// check if cursor position is after or before media position
// replace placeholder with url or failure message
// calclulate change of cursor position
//TODO: check if placeholder already present in text body
// check if cursor position is after or before media position
// replace placeholder with url or failure message
// calclulate change of cursor position
const imagePrefix = '!';
const placeholderPrefix = 'Uploading... ';
const imagePrefix = '!';
const placeholderPrefix = 'Uploading... '
let newText = text;
let newSelection = selection;
let newText = text;
let newSelection = selection;
const _insertFormatedString = (text, value) => {
const formatedText = `\n${imagePrefix}[${text}](${value})\n`;
newText = replaceBetween(newText, newSelection, formatedText);
const newIndex = newText && newText.indexOf(value, newSelection.start) + value.length + 2;
newSelection = {
start: newIndex,
end: newIndex,
};
};
const _replaceFormatedString = (placeholder: string, url: string) => {
const replaceStr = `(${placeholder})`;
const _insertFormatedString = (text, value) => {
const formatedText = `\n${imagePrefix}[${text}](${value})\n`;
newText = replaceBetween(newText, newSelection, formatedText);
const newIndex = newText && newText.indexOf(value, newSelection.start) + value.length + 2;
newSelection = {
start: newIndex,
end: newIndex
}
const endingIndex = newText.indexOf(replaceStr) + replaceStr.length + 1;
newText = newText.replace(replaceStr, `(${url})`);
if (newSelection.start >= endingIndex) {
const lengthDiff = url.length - placeholder.length;
newSelection = {
start: newSelection.start + lengthDiff,
end: newSelection.end + lengthDiff,
};
}
};
const _removeFormatedString = (placeholder) => {
const formatedText = `${imagePrefix}[](${placeholder})`;
const formatedTextIndex = newText.indexOf(formatedText);
newText = newText.replace(formatedText, '');
const _replaceFormatedString = (placeholder: string, url: string) => {
const replaceStr = `(${placeholder})`;
const endingIndex = newText.indexOf(replaceStr) + replaceStr.length + 1;
newText = newText.replace(replaceStr, `(${url})`);
if (newSelection.start >= endingIndex) {
const lengthDiff = url.length - placeholder.length
newSelection = {
start: newSelection.start + lengthDiff,
end: newSelection.end + lengthDiff
}
}
if (newSelection.start > formatedTextIndex) {
newSelection = {
start: newSelection.start - formatedText.length,
end: newSelection.end - formatedText.length,
};
}
};
items.forEach((item) => {
const _placeholder = item.filename && `${placeholderPrefix}${item.filename}`;
const _removeFormatedString = (placeholder) => {
const formatedText = `${imagePrefix}[](${placeholder})`
const formatedTextIndex = newText.indexOf(formatedText);
newText = newText.replace(formatedText, '')
switch (item.status) {
case MediaInsertStatus.UPLOADING: //means only filename is available
if (!_placeholder) return;
_insertFormatedString(item.text, _placeholder);
break;
if (newSelection.start > formatedTextIndex) {
newSelection = {
start: newSelection.start - formatedText.length,
end: newSelection.end - formatedText.length
}
case MediaInsertStatus.READY: //means url is ready but filename may be available
if (_placeholder && newText.includes(_placeholder)) {
//means placeholder is preset is needs replacing
_replaceFormatedString(_placeholder, item.url);
} else if (item.url) {
_insertFormatedString(item.text, item.url);
}
break;
case MediaInsertStatus.FAILED: //filename available but upload failed
if (_placeholder && newText.includes(_placeholder)) {
_removeFormatedString(_placeholder);
}
break;
default:
if (item.url) {
_insertFormatedString(item.text, item.url);
}
break;
}
});
items.forEach(item => {
const _placeholder = item.filename && `${placeholderPrefix}${item.filename}`;
switch (item.status) {
case MediaInsertStatus.UPLOADING: //means only filename is available
if (!_placeholder) return;
_insertFormatedString(item.text, _placeholder)
break;
case MediaInsertStatus.READY: //means url is ready but filename may be available
if (_placeholder && newText.includes(_placeholder)) {
//means placeholder is preset is needs replacing
_replaceFormatedString(_placeholder, item.url);
} else if (item.url) {
_insertFormatedString(item.text, item.url);
}
break;
case MediaInsertStatus.FAILED: //filename available but upload failed
if (_placeholder && newText.includes(_placeholder)) {
_removeFormatedString(_placeholder);
}
break;
default:
if (item.url) {
_insertFormatedString(item.text, item.url);
}
break;
}
});
setTextAndSelection({ text: newText, selection: newSelection });
setTextAndSelection({ text: newText, selection: newSelection });
};

View File

@ -1,10 +1,10 @@
import {replaceBetween } from './utils';
import { replaceBetween } from './utils';
export default async ({ text, selection, setTextAndSelection, snippetText}) => {
export default async ({ text, selection, setTextAndSelection, snippetText }) => {
const newText = replaceBetween(text, selection, `${snippetText}`);
const newSelection = {
start: selection.start,
end: selection.start + (snippetText && snippetText.length),
};
const newSelection = {
start: selection.start,
end: selection.start + (snippetText && snippetText.length),
};
setTextAndSelection({ text: newText, selection: newSelection });
};
};

View File

@ -1,11 +1,15 @@
import { extractWordAtIndex } from '../../../../utils/editor';
import {replaceBetween } from './utils';
import { replaceBetween } from './utils';
export default async ({ text, selection, setTextAndSelection, username}) => {
const _word = extractWordAtIndex(text, selection.start);
const _insertAt = text.indexOf(_word, selection.start - _word.length);
const _text = replaceBetween(text, {start:_insertAt, end:_insertAt + _word.length}, `@${username} `)
const _newPos = _insertAt + username.length + 2;
const _selection = { start: _newPos, end: _newPos };
setTextAndSelection({ selection: _selection, text: _text });
};
export default async ({ text, selection, setTextAndSelection, username }) => {
const _word = extractWordAtIndex(text, selection.start);
const _insertAt = text.indexOf(_word, selection.start - _word.length);
const _text = replaceBetween(
text,
{ start: _insertAt, end: _insertAt + _word.length },
`@${username} `,
);
const _newPos = _insertAt + username.length + 2;
const _selection = { start: _newPos, end: _newPos };
setTextAndSelection({ selection: _selection, text: _text });
};

View File

@ -1,93 +1,104 @@
import React, {useState, useEffect, useCallback} from 'react';
import { View, FlatList, Text, TouchableOpacity } from "react-native"
import React, { useState, useEffect, useCallback } from 'react';
import { View, FlatList, Text, TouchableOpacity } from 'react-native';
import {debounce} from 'lodash';
import { UserAvatar } from '../..';
import { lookupAccounts } from '../../../providers/hive/dhive';
import { extractWordAtIndex } from '../../../utils/editor';
import styles from '../styles/markdownEditorStyles';
import {debounce} from 'lodash';
interface Props {
text:string,
selection:{
start:number,
end:number
}
onApplyUsername:(username:string)=>void;
text: string;
selection: {
start: number;
end: number;
};
onApplyUsername: (username: string) => void;
}
export const UsernameAutofillBar = ({text, selection, onApplyUsername}:Props) => {
export const UsernameAutofillBar = ({ text, selection, onApplyUsername }: Props) => {
const [searchedUsers, setSearchedUsers] = useState([]);
const [query, setQuery] = useState('');
const [searchedUsers, setSearchedUsers] = useState([])
const [query, setQuery] = useState('');
useEffect(() => {
if (selection.start === selection.end && text) {
_processTextForSearch(text, selection.start);
}
}, [text, selection]);
useEffect(() => {
if (selection.start === selection.end && text) {
_processTextForSearch(text, selection.start);
}
}, [text, selection])
const _processTextForSearch = useCallback(debounce((text:string, index:number) => {
const word = extractWordAtIndex(text, index);
console.log('selection word is: ', word);
if (word.startsWith('@') && word.length > 1) {
_handleUserSearch(word.substring(1));
} else {
setSearchedUsers([]);
setQuery('')
_handleUserSearch.cancel();
}
}, 300, {leading:true}),[]);
const _handleUserSearch = useCallback(debounce(async (username) => {
if(query !== username){
let users = [];
if (username) {
setQuery(username)
users = await lookupAccounts(username);
console.log('result users for', username, users);
const _processTextForSearch = useCallback(
debounce(
(text: string, index: number) => {
const word = extractWordAtIndex(text, index);
console.log('selection word is: ', word);
if (word.startsWith('@') && word.length > 1) {
_handleUserSearch(word.substring(1));
} else {
setSearchedUsers([]);
setQuery('');
_handleUserSearch.cancel();
}
setSearchedUsers(users);
}
}, 200, {leading:true}), []);
},
300,
{ leading: true },
),
[],
);
const _handleUserSearch = useCallback(
debounce(
async (username) => {
if (query !== username) {
let users = [];
if (username) {
setQuery(username);
users = await lookupAccounts(username);
console.log('result users for', username, users);
}
setSearchedUsers(users);
}
const _onUserSelect = (username) => {
onApplyUsername(username)
setSearchedUsers([]);
setQuery('')
};
200,
{ leading: true },
),
if(!searchedUsers || searchedUsers.length === 0 || query === ''){
return null;
}
);
const _renderItem = ({item}:{item:string}) => {
const _onUserSelect = (username) => {
onApplyUsername(username);
setSearchedUsers([]);
setQuery('');
};
const username = item;
return (
<TouchableOpacity onPress={()=>{_onUserSelect(username)}}>
<View style={styles.userBubble}>
<UserAvatar username={username}/>
<Text style={styles.userBubbleText}>{username}</Text>
</View>
</TouchableOpacity>
)
}
return (
<View style={styles.searchAccountsContainer}>
<FlatList
horizontal={true}
data={searchedUsers}
keyboardShouldPersistTaps="always"
showsHorizontalScrollIndicator={false}
renderItem={_renderItem}
keyExtractor={(item)=>`searched-user-${item}`}
/>
</View>
)
if (!searchedUsers || searchedUsers.length === 0 || query === '') {
return null;
}
const _renderItem = ({ item }: { item: string }) => {
const username = item;
return (
<TouchableOpacity
onPress={() => {
_onUserSelect(username);
}}
>
<View style={styles.userBubble}>
<UserAvatar username={username} />
<Text style={styles.userBubbleText}>{username}</Text>
</View>
</TouchableOpacity>
);
};
return (
<View style={styles.searchAccountsContainer}>
<FlatList
horizontal={true}
data={searchedUsers}
keyboardShouldPersistTaps="always"
showsHorizontalScrollIndicator={false}
renderItem={_renderItem}
keyExtractor={(item) => `searched-user-${item}`}
/>
</View>
);
};

View File

@ -10,11 +10,11 @@ const _dropShadow = {
},
backgroundColor: '$primaryBackgroundColor',
borderColor: '$primaryLightBackground',
borderTopWidth : Platform.select({
borderTopWidth: Platform.select({
android: 1,
ios: 0
})
}
ios: 0,
}),
};
export default EStyleSheet.create({
container: {
@ -22,13 +22,13 @@ export default EStyleSheet.create({
elevation: 3,
backgroundColor: '$primaryBackgroundColor',
},
shadowedContainer:{
shadowedContainer: {
elevation: 3,
width: '$deviceWidth',
..._dropShadow
..._dropShadow,
},
dropShadow: {
..._dropShadow
..._dropShadow,
},
buttonsContainer: {
justifyContent: 'space-between',
@ -36,7 +36,7 @@ export default EStyleSheet.create({
width: '$deviceWidth',
backgroundColor: '$primaryBackgroundColor',
borderColor: '$primaryLightBackground',
paddingBottom: getBottomSpace()
paddingBottom: getBottomSpace(),
},
clearIcon: {
color: '$primaryLightGray',
@ -75,8 +75,6 @@ export default EStyleSheet.create({
backgroundColor: '$primaryLightBackground',
borderRadius: 8,
margin: 8,
alignSelf: 'center'
}
alignSelf: 'center',
},
});

View File

@ -50,7 +50,7 @@ import { useAppSelector } from '../../../hooks';
const MIN_BODY_INPUT_HEIGHT = 300;
//These variable keep track of body text input state,
//These variable keep track of body text input state,
//this helps keep load on minimal compared to both useState and useRef;
var bodyText = '';
var bodySelection = { start: 0, end: 0 };
@ -74,11 +74,11 @@ const MarkdownEditorView = ({
autoFocusText,
sharedSnippetText,
onLoadDraftPress,
setIsUploading
setIsUploading,
}) => {
const dispatch = useDispatch();
const isDarkTheme = useAppSelector(state => state.application.isDarkTheme);
const isDarkTheme = useAppSelector((state) => state.application.isDarkTheme);
const [editable, setEditable] = useState(true);
const [bodyInputHeight, setBodyInputHeight] = useState(MIN_BODY_INPUT_HEIGHT);
@ -87,7 +87,6 @@ const MarkdownEditorView = ({
const [isEditing, setIsEditing] = useState(false);
const [insertedMediaUrls, setInsertedMediaUrls] = useState([]);
const inputRef = useRef(null);
const clearRef = useRef(null);
const insertLinkModalRef = useRef(null);
@ -164,7 +163,6 @@ const MarkdownEditorView = ({
}
}, [isLoading]);
useEffect(() => {
bodyText = draftBody;
}, [draftBody]);
@ -178,8 +176,6 @@ const MarkdownEditorView = ({
}
}, [autoFocusText]);
const changeUser = async () => {
dispatch(toggleAccountsBottomSheet(!isVisibleAccountsBottomSheet));
};
@ -193,33 +189,37 @@ const MarkdownEditorView = ({
});
};
const _debouncedOnTextChange = useCallback(debounce(() => {
console.log("setting is editing to", false)
setIsEditing(false)
const urls = extractImageUrls({ body: bodyText })
if (urls.length !== insertedMediaUrls.length) {
setInsertedMediaUrls(urls);
}
}, 500), [])
const _debouncedOnTextChange = useCallback(
debounce(() => {
console.log('setting is editing to', false);
setIsEditing(false);
const urls = extractImageUrls({ body: bodyText });
if (urls.length !== insertedMediaUrls.length) {
setInsertedMediaUrls(urls);
}
}, 500),
[],
);
// eslint-disable-next-line react-hooks/exhaustive-deps
const _changeText = useCallback((input) => {
bodyText = input;
const _changeText = useCallback(
(input) => {
bodyText = input;
if (!isEditing) {
console.log('force setting is editing to true', true)
setIsEditing(true)
}
if (!isEditing) {
console.log('force setting is editing to true', true);
setIsEditing(true);
}
_debouncedOnTextChange();
//NOTE: onChange method is called by direct parent of MarkdownEditor that is PostForm, do not remove
if (onChange) {
onChange(input);
}
}, [isEditing]);
_debouncedOnTextChange();
//NOTE: onChange method is called by direct parent of MarkdownEditor that is PostForm, do not remove
if (onChange) {
onChange(input);
}
},
[isEditing],
);
const _handleOnSelectionChange = async (event) => {
bodySelection = event.nativeEvent.selection;
@ -233,11 +233,11 @@ const MarkdownEditorView = ({
});
const _updateSelection = () => {
bodySelection = _selection
bodySelection = _selection;
inputRef?.current?.setNativeProps({
selection: _selection,
});
}
};
// Workaround for iOS selection update issue
if (Platform.OS === 'ios') {
@ -245,7 +245,7 @@ const MarkdownEditorView = ({
_updateSelection();
}, 100);
} else {
_updateSelection()
_updateSelection();
}
if (isSnippetsOpen) {
@ -275,8 +275,6 @@ const MarkdownEditorView = ({
setIsSnippetsOpen(false);
};
const _handleMediaInsert = (mediaArray: MediaInsertData[]) => {
if (mediaArray.length) {
applyMediaLink({
@ -288,9 +286,6 @@ const MarkdownEditorView = ({
}
};
const _handleOnAddLinkPress = () => {
insertLinkModalRef.current?.showModal({
selectedText: bodyText.slice(bodySelection.start, bodySelection.end),
@ -314,7 +309,6 @@ const MarkdownEditorView = ({
insertLinkModalRef.current?.hideModal();
};
const _renderFloatingDraftButton = () => {
if (showDraftLoadButton) {
const _onPress = () => {
@ -354,7 +348,7 @@ const MarkdownEditorView = ({
_setTextAndSelection({ text: '', selection: { start: 0, end: 0 } });
}
};
const _renderEditor = (editorScrollEnabled:boolean) => (
const _renderEditor = (editorScrollEnabled: boolean) => (
<>
{isReply && !isEdit && <SummaryArea summary={headerText} />}
{!isReply && (
@ -397,38 +391,44 @@ const MarkdownEditorView = ({
</View>
)}
{!isPreviewActive ? (
<TextInput
multiline
autoCorrect={true}
autoFocus={!draftBtnTooltipRegistered ? false : true}
onChangeText={_changeText}
onSelectionChange={_handleOnSelectionChange}
placeholder={intl.formatMessage({
id: isReply ? 'editor.reply_placeholder' : 'editor.default_placeholder',
})}
placeholderTextColor={isDarkTheme ? '#526d91' : '#c1c5c7'}
selectionColor="#357ce6"
style={styles.textWrapper}
underlineColorAndroid="transparent"
innerRef={inputRef}
editable={editable}
contextMenuHidden={false}
scrollEnabled={editorScrollEnabled}
/>
<TextInput
multiline
autoCorrect={true}
autoFocus={!draftBtnTooltipRegistered ? false : true}
onChangeText={_changeText}
onSelectionChange={_handleOnSelectionChange}
placeholder={intl.formatMessage({
id: isReply ? 'editor.reply_placeholder' : 'editor.default_placeholder',
})}
placeholderTextColor={isDarkTheme ? '#526d91' : '#c1c5c7'}
selectionColor="#357ce6"
style={styles.textWrapper}
underlineColorAndroid="transparent"
innerRef={inputRef}
editable={editable}
contextMenuHidden={false}
scrollEnabled={editorScrollEnabled}
/>
) : (
_renderPreview()
)}
</>
);
const _editorWithScroll = <ScrollView style={styles.container}>{_renderEditor(false)}</ScrollView>;
const _editorWithScroll = (
<ScrollView style={styles.container}>{_renderEditor(false)}</ScrollView>
);
const _editorWithoutScroll = <View style={styles.container}>{_renderEditor(true)}</View>;
const _renderContent = () => {
const _innerContent = (
<>
{isAndroidOreo() ? _editorWithoutScroll : _editorWithScroll}
<UsernameAutofillBar text={bodyText} selection={bodySelection} onApplyUsername={_onApplyUsername} />
<UsernameAutofillBar
text={bodyText}
selection={bodySelection}
onApplyUsername={_onApplyUsername}
/>
{_renderFloatingDraftButton()}
<EditorToolbar
@ -445,11 +445,10 @@ const MarkdownEditorView = ({
text: bodyText,
selection: bodySelection,
setTextAndSelection: _setTextAndSelection,
item
})
item,
});
}}
/>
</>
);
@ -480,8 +479,6 @@ const MarkdownEditorView = ({
<SnippetsModal handleOnSelect={_handleOnSnippetReceived} />
</Modal>
<InsertLinkModal
ref={insertLinkModalRef}
handleOnInsertLink={_handleInsertLink}

View File

@ -1,2 +1,2 @@
export * from './quickProfileModal';
export * from './inputSupportModal';
export * from './inputSupportModal';

View File

@ -1,12 +1,10 @@
import {ViewStyle } from 'react-native';
import { ViewStyle } from 'react-native';
import EStyleSheet from 'react-native-extended-stylesheet';
export default EStyleSheet.create({
container: {
flex: 1,
justifyContent:"flex-end",
backgroundColor: 'rgba(0, 0, 0, 0.2)',
} as ViewStyle,
})
container: {
flex: 1,
justifyContent: 'flex-end',
backgroundColor: 'rgba(0, 0, 0, 0.2)',
} as ViewStyle,
});

View File

@ -1,17 +1,16 @@
import React, { useEffect, useRef, useState } from 'react';
import { View as AnimatedView } from 'react-native-animatable'
import { View as AnimatedView } from 'react-native-animatable';
import { Portal } from 'react-native-portalize';
import styles from '../children/inputSupportModal.styles';
import { KeyboardAvoidingView, Platform, View } from 'react-native';
import styles from '../children/inputSupportModal.styles';
export interface InputSupportModalProps {
visible: boolean;
onClose: () => void;
children?: any
children?: any;
}
export const InputSupportModal = ({ children, visible, onClose }: InputSupportModalProps, ref) => {
const container = useRef<AnimatedView>(null);
const innerContainer = useRef<AnimatedView>(null);
@ -20,48 +19,38 @@ export const InputSupportModal = ({ children, visible, onClose }: InputSupportMo
useEffect(() => {
if (visible) {
setShowModal(true);
}
else if (!visible && container.current && innerContainer.current) {
innerContainer.current.slideOutDown(1000)
} else if (!visible && container.current && innerContainer.current) {
innerContainer.current.slideOutDown(1000);
setTimeout(async () => {
await container.current?.fadeOut(200)
await container.current?.fadeOut(200);
setShowModal(false);
}, 300)
}, 300);
}
}, [visible])
}, [visible]);
return showModal && (
<Portal>
<AnimatedView
ref={container}
animation='fadeIn'
duration={300}
style={styles.container} >
<AnimatedView
ref={innerContainer}
style={{ flex: 1 }}
animation='slideInUp'
duration={300}>
<View
return (
showModal && (
<Portal>
<AnimatedView ref={container} animation="fadeIn" duration={300} style={styles.container}>
<AnimatedView
ref={innerContainer}
style={{ flex: 1 }}
onTouchEnd={onClose} />
animation="slideInUp"
duration={300}
>
<View style={{ flex: 1 }} onTouchEnd={onClose} />
{
Platform.select({
{Platform.select({
ios: (
<KeyboardAvoidingView behavior="padding" style={{}}>
{children}
</KeyboardAvoidingView>
),
android: <View>{children}</View>,
})
}
})}
</AnimatedView>
</AnimatedView>
</AnimatedView>
</Portal>
</Portal>
)
);
};

View File

@ -1 +1 @@
export * from './container/inputSupportModal';
export * from './container/inputSupportModal';

View File

@ -82,11 +82,11 @@ class PostDropdownContainer extends PureComponent {
const _canUpdateCommunityPin =
subscribedCommunities.data && !!content && content.community
? subscribedCommunities.data.reduce((role, subscription) => {
if (content.community === subscription[0]) {
return ['owner', 'admin', 'mod'].includes(subscription[2]);
}
return role;
}, false)
if (content.community === subscription[0]) {
return ['owner', 'admin', 'mod'].includes(subscription[2]);
}
return role;
}, false)
: false;
const _isPinnedInCommunity = !!content && content.stats?.is_pinned;
@ -290,7 +290,7 @@ class PostDropdownContainer extends PureComponent {
buttons: [
{
text: intl.formatMessage({ id: 'alert.cancel' }),
onPress: () => { },
onPress: () => {},
},
{
text: intl.formatMessage({ id: 'alert.confirm' }),
@ -329,17 +329,16 @@ class PostDropdownContainer extends PureComponent {
};
_reblog = () => {
const {
content,
currentAccount,
dispatch,
intl,
isLoggedIn,
pinCode,
const {
content,
currentAccount,
dispatch,
intl,
isLoggedIn,
pinCode,
navigation,
userActivityMutation
} = this
.props as any;
userActivityMutation,
} = this.props as any;
if (!isLoggedIn) {
showLoginAlert({ navigation, intl });
return;
@ -349,9 +348,9 @@ class PostDropdownContainer extends PureComponent {
.then((response) => {
//track user activity points ty=130
userActivityMutation.mutate({
pointsTy:PointActivityIds.REBLOG,
transactionId:response.id
})
pointsTy: PointActivityIds.REBLOG,
transactionId: response.id,
});
dispatch(
toastNotification(
@ -517,9 +516,11 @@ const mapStateToProps = (state) => ({
});
const mapQueriesToProps = () => ({
userActivityMutation: useUserActivityMutation()
})
userActivityMutation: useUserActivityMutation(),
});
export default withNavigation(connect(mapStateToProps)(injectIntl((props) => (
<PostDropdownContainer {...props} {...mapQueriesToProps()} />)))
export default withNavigation(
connect(mapStateToProps)(
injectIntl((props) => <PostDropdownContainer {...props} {...mapQueriesToProps()} />),
),
);

View File

@ -1,4 +1,4 @@
import React, { Fragment, useState, useRef } from 'react';
import React, { Fragment, useState, useRef, useCallback } from 'react';
import { Linking, Modal, PermissionsAndroid, Platform, View } from 'react-native';
import { useIntl } from 'react-intl';
import CameraRoll from '@react-native-community/cameraroll';
@ -8,6 +8,7 @@ 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 { navigate } from '../../../../navigation/service';
// Constants
@ -21,8 +22,7 @@ import styles from './commentBodyStyles';
// Services and Actions
import { writeToClipboard } from '../../../../utils/clipboard';
import { toastNotification } from '../../../../redux/actions/uiAction';
import { LongPressGestureHandler, State } from 'react-native-gesture-handler';
import { useCallback } from 'react';
import { OptionsModal } from '../../../atoms';
import { useAppDispatch } from '../../../../hooks';
import { isCommunity } from '../../../../utils/communityValidation';
@ -39,10 +39,9 @@ const CommentBody = ({
created,
commentDepth,
reputation = 25,
isMuted
isMuted,
}) => {
const _contentWidth = WIDTH - (40 + 28 + (commentDepth > 2 ? 44 : 0))
const _contentWidth = WIDTH - (40 + 28 + (commentDepth > 2 ? 44 : 0));
const dispatch = useAppDispatch();
@ -52,7 +51,7 @@ const CommentBody = ({
const [selectedLink, setSelectedLink] = useState(null);
const [revealComment, setRevealComment] = useState(reputation > 0 && !isMuted);
const [videoUrl, setVideoUrl] = useState(null);
const [youtubeVideoId, setYoutubeVideoId] = useState(null)
const [youtubeVideoId, setYoutubeVideoId] = useState(null);
const [videoStartTime, setVideoStartTime] = useState(0);
const intl = useIntl();
@ -60,18 +59,16 @@ const CommentBody = ({
const actionLink = useRef(null);
const youtubePlayerRef = useRef(null);
const _onLongPressStateChange = ({nativeEvent}) => {
if(nativeEvent.state === State.ACTIVE){
const _onLongPressStateChange = ({ nativeEvent }) => {
if (nativeEvent.state === State.ACTIVE) {
handleOnLongPress();
}
}
};
const _showLowComment = () => {
setRevealComment(true);
};
const handleImagePress = (ind) => {
if (ind === 1) {
//open gallery mode
@ -128,12 +125,11 @@ const CommentBody = ({
);
});
}
setSelectedLink(null);
};
const _handleTagPress = (tag:string, filter:string = GLOBAL_POST_FILTERS_VALUE[0]) => {
const _handleTagPress = (tag: string, filter: string = GLOBAL_POST_FILTERS_VALUE[0]) => {
if (tag) {
const routeName = isCommunity(tag) ? ROUTES.SCREENS.COMMUNITY : ROUTES.SCREENS.TAG_RESULT;
const key = `${filter}/${tag}`;
@ -148,21 +144,21 @@ const CommentBody = ({
}
};
const _handleSetSelectedLink = (link:string) => {
setSelectedLink(link)
const _handleSetSelectedLink = (link: string) => {
setSelectedLink(link);
actionLink.current.show();
}
};
const _handleSetSelectedImage = (imageLink:string, postImgUrls:string[]) => {
if(postImages.length !== postImgUrls.length){
const _handleSetSelectedImage = (imageLink: string, postImgUrls: string[]) => {
if (postImages.length !== postImgUrls.length) {
setPostImages(postImgUrls);
}
setSelectedImage(imageLink);
actionImage.current.show();
}
};
const _handleOnPostPress = (permlink, author) => {
if(handleOnPostPress){
if (handleOnPostPress) {
handleOnUserPress(permlink, author);
return;
}
@ -179,7 +175,7 @@ const CommentBody = ({
};
const _handleOnUserPress = (username) => {
if(handleOnUserPress){
if (handleOnUserPress) {
handleOnUserPress(username);
return;
}
@ -279,17 +275,16 @@ const CommentBody = ({
const _handleVideoPress = (embedUrl) => {
if (embedUrl && youtubePlayerRef.current) {
setVideoUrl(embedUrl);
setVideoStartTime(0)
setVideoStartTime(0);
youtubePlayerRef.current.setModalVisible(true);
}
};
return (
<Fragment>
<Modal key={`mkey-${created.toString()}`} visible={isImageModalOpen} transparent={true}>
<ImageViewer
imageUrls={postImages.map((url)=>({url}))}
imageUrls={postImages.map((url) => ({ url }))}
enableSwipeDown
onCancel={() => setIsImageModalOpen(false)}
onClick={() => setIsImageModalOpen(false)}
@ -339,7 +334,6 @@ const CommentBody = ({
/>
</View>
</LongPressGestureHandler>
) : (
<TextButton
style={styles.revealButton}
@ -359,11 +353,11 @@ const CommentBody = ({
setVideoUrl(null);
}}
>
<VideoPlayer
<VideoPlayer
mode={youtubeVideoId ? 'youtube' : 'uri'}
youtubeVideoId={youtubeVideoId}
uri={videoUrl}
startTime={videoStartTime}
youtubeVideoId={youtubeVideoId}
uri={videoUrl}
startTime={videoStartTime}
/>
</ActionsSheetView>
</Fragment>
@ -371,4 +365,3 @@ const CommentBody = ({
};
export default CommentBody;

View File

@ -1,21 +1,21 @@
import React, { memo, useMemo, useRef } from 'react';
import RenderHTML, { CustomRendererProps, Element, TNode } from 'react-native-render-html';
import { useHtmlIframeProps, iframeModel } from '@native-html/iframe-plugin';
import WebView from 'react-native-webview';
import { ScrollView } from 'react-native-gesture-handler';
import { prependChild, removeElement } from 'htmlparser2/node_modules/domutils';
import styles from './postHtmlRendererStyles';
import { LinkData, parseLinkData } from './linkDataParser';
import VideoThumb from './videoThumb';
import { AutoHeightImage } from '../autoHeightImage/autoHeightImage';
import WebView from 'react-native-webview';
import { VideoPlayer } from '..';
import { ScrollView } from 'react-native-gesture-handler';
import { prependChild, removeElement } from 'htmlparser2/node_modules/domutils';
interface PostHtmlRendererProps {
contentWidth: number;
body: string;
isComment?: boolean;
onLoaded?: () => void;
setSelectedImage: (imgUrl: string, postImageUrls:string[]) => void;
setSelectedImage: (imgUrl: string, postImageUrls: string[]) => void;
setSelectedLink: (url: string) => void;
handleOnPostPress: (permlink: string, authro: string) => void;
handleOnUserPress: (username: string) => void;
@ -38,7 +38,6 @@ export const PostHtmlRenderer = memo(
handleVideoPress,
handleYoutubePress,
}: PostHtmlRendererProps) => {
const postImgUrlsRef = useRef<string[]>([]);
//new renderer functions
@ -146,8 +145,8 @@ export const PostHtmlRenderer = memo(
if (element.tagName === 'img' && element.attribs.src) {
const imgUrl = element.attribs.src;
console.log('img element detected', imgUrl);
if(!postImgUrlsRef.current.includes(imgUrl)){
postImgUrlsRef.current.push(imgUrl)
if (!postImgUrlsRef.current.includes(imgUrl)) {
postImgUrlsRef.current.push(imgUrl);
}
}
@ -291,7 +290,6 @@ export const PostHtmlRenderer = memo(
);
};
// iframe renderer for rendering iframes in body
const _iframeRenderer = function IframeRenderer(props) {
const iframeProps = useHtmlIframeProps(props);

View File

@ -1,5 +1,5 @@
import { ImageStyle, Platform } from 'react-native';
import { ViewStyle, TextStyle } from 'react-native';
import { ImageStyle, Platform, ViewStyle, TextStyle } from 'react-native';
import EStyleSheet from 'react-native-extended-stylesheet';
export default EStyleSheet.create({
@ -13,82 +13,82 @@ export default EStyleSheet.create({
color: '$primaryBlack',
} as TextStyle,
div: {
width:'100%',
},
p:{
marginTop:6,
marginBottom:6,
flexDirection:'row',
alignItems:'center',
flexWrap:'wrap'
} as TextStyle,
h6:{
fontSize:14,
} as TextStyle,
pLi:{
marginTop:0,
marginBottom:0
} as TextStyle,
a:{
color: '$primaryBlue'
} as TextStyle,
img:{
width: '100%',
alignSelf:'center',
marginTop:4,
marginBottom:4,
backgroundColor:'red'
},
p: {
marginTop: 6,
marginBottom: 6,
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
} as TextStyle,
h6: {
fontSize: 14,
} as TextStyle,
pLi: {
marginTop: 0,
marginBottom: 0,
} as TextStyle,
a: {
color: '$primaryBlue',
} as TextStyle,
img: {
width: '100%',
alignSelf: 'center',
marginTop: 4,
marginBottom: 4,
backgroundColor: 'red',
} as ImageStyle,
th:{
th: {
flex: 1,
justifyContent: 'center',
alignItems:'center',
alignItems: 'center',
fontWeight: 'bold',
color: '$primaryBlack',
backgroundColor:'$darkIconColor',
backgroundColor: '$darkIconColor',
fontSize: 14,
padding: 10,
} as TextStyle,
tr:{
flexDirection:'row',
tr: {
flexDirection: 'row',
} as ViewStyle,
td:{
flex:1,
td: {
flex: 1,
borderWidth: 0.5,
padding:10,
padding: 10,
borderColor: '$tableBorderColor',
backgroundColor: '$tableTrColor',
alignItems:'center',
justifyContent:'center',
alignItems: 'center',
justifyContent: 'center',
} as ViewStyle,
table:{
width: '100%',
table: {
width: '100%',
} as ViewStyle,
li:{
marginBottom:12
li: {
marginBottom: 12,
} as ViewStyle,
blockquote: {
borderLeftWidth: 5,
borderStyle:'solid',
marginLeft:5,
paddingLeft:5,
borderStyle: 'solid',
marginLeft: 5,
paddingLeft: 5,
borderColor: '$darkIconColor',
} as ViewStyle,
code:{
backgroundColor:'$darkIconColor',
fontFamily:'$editorFont',
code: {
backgroundColor: '$darkIconColor',
fontFamily: '$editorFont',
} as TextStyle,
textCenter: {
textAlign: 'center',
alignItems: 'center',
justifyContent: 'center',
} as TextStyle,
phishy:{
color:'$primaryRed',
phishy: {
color: '$primaryRed',
} as TextStyle,
textJustify:{
textAlign: Platform.select({ios:'justify', android:'auto'}), //justify with selectable on android causes ends of text getting clipped,
letterSpacing:0
textJustify: {
textAlign: Platform.select({ ios: 'justify', android: 'auto' }), //justify with selectable on android causes ends of text getting clipped,
letterSpacing: 0,
} as TextStyle,
revealButton: {
backgroundColor: '$iconColor',
@ -103,20 +103,18 @@ export default EStyleSheet.create({
color: '$white',
fontSize: 14,
},
videoThumb:{
width:'100%',
alignItems:'center',
justifyContent:'center',
backgroundColor:'$darkIconColor'
videoThumb: {
width: '100%',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '$darkIconColor',
},
playButton:{
alignItems:'center',
justifyContent:'center',
playButton: {
alignItems: 'center',
justifyContent: 'center',
width: 60,
height: 60,
borderRadius: 30,
backgroundColor:'$primaryBlack'
} as ViewStyle
backgroundColor: '$primaryBlack',
} as ViewStyle,
});

View File

@ -70,8 +70,8 @@ const PostDisplayView = ({
if (isLoggedIn && get(currentAccount, 'name') && !isNewPost) {
//track user activity for view post
userActivityMutation.mutate({
pointsTy:PointActivityIds.VIEW_POST
})
pointsTy: PointActivityIds.VIEW_POST,
});
}
}, []);

View File

@ -1,28 +1,29 @@
import React, {forwardRef, memo, useRef, useImperativeHandle, useState, useEffect} from 'react'
import PostCard from '../../postCard';
import React, { forwardRef, memo, useRef, useImperativeHandle, useState, useEffect } from 'react';
import { get } from 'lodash';
import { FlatListProps, FlatList, RefreshControl, ActivityIndicator, View } from 'react-native';
import { useSelector } from 'react-redux';
import PostCard from '../../postCard';
import { ThemeContainer } from '../../../containers';
import styles from '../view/postsListStyles';
export interface PostsListRef {
scrollToTop:()=>void
scrollToTop: () => void;
}
interface postsListContainerProps extends FlatListProps<any> {
promotedPosts:Array<any>;
isFeedScreen:boolean;
onLoadPosts?:(shouldReset:boolean)=>void;
isLoading:boolean;
isRefreshing:boolean;
pageType:'main'|'profile'|'ownProfile'|'community';
showQuickReplyModal:(post:any)=>void;
promotedPosts: Array<any>;
isFeedScreen: boolean;
onLoadPosts?: (shouldReset: boolean) => void;
isLoading: boolean;
isRefreshing: boolean;
pageType: 'main' | 'profile' | 'ownProfile' | 'community';
showQuickReplyModal: (post: any) => void;
}
let _onEndReachedCalledDuringMomentum = true;
const postsListContainer = ({
const postsListContainer = (
{
promotedPosts,
isFeedScreen,
onLoadPosts,
@ -31,65 +32,61 @@ const postsListContainer = ({
pageType,
showQuickReplyModal,
...props
}:postsListContainerProps, ref) => {
}: postsListContainerProps,
ref,
) => {
const flatListRef = useRef(null);
const [imageHeights, setImageHeights] = useState(new Map<string, number>());
const [imageHeights, setImageHeights] = useState(new Map<string, number>());
const isHideImages = useSelector((state) => state.application.hidePostsThumbnails);
const posts = useSelector((state) => {
return isFeedScreen
? state.posts.feedPosts
: state.posts.otherPosts
});
const mutes = useSelector((state) => state.account.currentAccount.mutes);
const scrollPosition = useSelector((state) => {
return isFeedScreen
? state.posts.feedScrollPosition
: state.posts.otherScrollPosition
const isHideImages = useSelector((state) => state.application.hidePostsThumbnails);
const posts = useSelector((state) => {
return isFeedScreen ? state.posts.feedPosts : state.posts.otherPosts;
});
const mutes = useSelector((state) => state.account.currentAccount.mutes);
const scrollPosition = useSelector((state) => {
return isFeedScreen ? state.posts.feedScrollPosition : state.posts.otherScrollPosition;
});
useImperativeHandle(ref, () => ({
scrollToTop() {
flatListRef.current?.scrollToOffset({ x: 0, y: 0, animated: true });
},
}));
useImperativeHandle(ref, () => ({
scrollToTop() {
flatListRef.current?.scrollToOffset({ x: 0, y: 0, animated: true });
},
}));
useEffect(() => {
console.log("Scroll Position: ", scrollPosition)
if(posts && posts.length == 0){
flatListRef.current?.scrollToOffset({
offset: 0,
animated: false
});
}
}, [posts])
useEffect(() => {
console.log("Scroll Position: ", scrollPosition)
useEffect(() => {
console.log('Scroll Position: ', scrollPosition);
if (posts && posts.length == 0) {
flatListRef.current?.scrollToOffset({
offset: (posts && posts.length == 0) ? 0 : scrollPosition,
animated: false
offset: 0,
animated: false,
});
}, [scrollPosition])
const _setImageHeightInMap = (mapKey:string, height:number) => {
if(mapKey && height){
setImageHeights(imageHeights.set(mapKey, height));
}
}
}, [posts]);
useEffect(() => {
console.log('Scroll Position: ', scrollPosition);
flatListRef.current?.scrollToOffset({
offset: posts && posts.length == 0 ? 0 : scrollPosition,
animated: false,
});
}, [scrollPosition]);
const _setImageHeightInMap = (mapKey: string, height: number) => {
if (mapKey && height) {
setImageHeights(imageHeights.set(mapKey, height));
}
};
const _renderFooter = () => {
if (isLoading && !isRefreshing) {
return (
<View style={styles.flatlistFooter}>
<ActivityIndicator animating size="large" color={'#2e3d51'} />
<ActivityIndicator animating size="large" color="#2e3d51" />
</View>
);
}
@ -104,96 +101,103 @@ const postsListContainer = ({
}
};
const _renderItem = ({ item, index }: { item: any; index: number }) => {
const e = [] as any;
const _renderItem = ({ item, index }:{item:any, index:number}) => {
const e = [] as any;
if (index % 3 === 0) {
const ix = index / 3 - 1;
if (promotedPosts[ix] !== undefined) {
const p = promotedPosts[ix];
let isMuted = mutes && mutes.indexOf(p.author) > -1;
if (index % 3 === 0) {
const ix = index / 3 - 1;
if (promotedPosts[ix] !== undefined) {
const p = promotedPosts[ix];
let 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);
if (
!isMuted &&
get(p, 'author', null) &&
posts &&
posts.filter((x) => x.permlink === p.permlink).length <= 0
e.push(
<PostCard
key={`${p.author}-${p.permlink}-prom`}
content={p}
isHideImage={isHideImages}
imageHeight={imgHeight}
pageType={pageType}
setImageHeight = {_setImageHeightInMap}
showQuickReplyModal={showQuickReplyModal}
mutes={mutes}
/>
);
}
}
}
let 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 localId = p.author + p.permlink;
const imgHeight = imageHeights.get(localId);
e.push(
<PostCard
key={`${item.author}-${item.permlink}`}
content={item}
key={`${p.author}-${p.permlink}-prom`}
content={p}
isHideImage={isHideImages}
imageHeight={imgHeight}
setImageHeight = {_setImageHeightInMap}
pageType={pageType}
setImageHeight={_setImageHeightInMap}
showQuickReplyModal={showQuickReplyModal}
mutes={mutes}
/>,
);
}
return e;
};
return (
<ThemeContainer>
{({ isDarkTheme }) => (
<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;
}
}
let 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);
e.push(
<PostCard
key={`${item.author}-${item.permlink}`}
content={item}
isHideImage={isHideImages}
imageHeight={imgHeight}
setImageHeight={_setImageHeightInMap}
pageType={pageType}
showQuickReplyModal={showQuickReplyModal}
mutes={mutes}
/>,
);
}
return e;
};
return (
<ThemeContainer>
{({ isDarkTheme }) => (
<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);
}
}}
ListFooterComponent={_renderFooter}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={()=>{if(onLoadPosts){onLoadPosts(true)}}}
progressBackgroundColor="#357CE6"
tintColor={!isDarkTheme ? '#357ce6' : '#96c0ff'}
titleColor="#fff"
colors={['#fff']}
/>
}
{...props}
/>
)}
</ThemeContainer>
)
}
progressBackgroundColor="#357CE6"
tintColor={!isDarkTheme ? '#357ce6' : '#96c0ff'}
titleColor="#fff"
colors={['#fff']}
/>
}
{...props}
/>
)}
</ThemeContainer>
);
};
export default forwardRef(postsListContainer);

View File

@ -1,133 +1,130 @@
import React, {useEffect, useState} from 'react';
import React, { useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
import {ActivityIndicator, RefreshControl, View } from 'react-native';
import { ActivityIndicator, RefreshControl, View } from 'react-native';
import { unionBy } from 'lodash';
import { Comments, NoPost } from '../..';
import { useAppSelector } from '../../../hooks';
import { getAccountPosts } from '../../../providers/hive/dhive';
import styles from '../profileStyles';
import {unionBy } from 'lodash';
interface CommentsTabContentProps {
username:string,
type:'comments'|'replies',
isOwnProfile:boolean,
selectedUser:any,
onScroll:()=>void,
username: string;
type: 'comments' | 'replies';
isOwnProfile: boolean;
selectedUser: any;
onScroll: () => void;
}
const CommentsTabContent = ({isOwnProfile, username, type, onScroll, selectedUser }: CommentsTabContentProps) => {
const CommentsTabContent = ({
isOwnProfile,
username,
type,
onScroll,
selectedUser,
}: CommentsTabContentProps) => {
const intl = useIntl();
const intl = useIntl();
const isHideImage = useAppSelector((state) => state.application.hidePostsThumbnails);
const isHideImage = useAppSelector(state => state.application.hidePostsThumbnails);
const [data, setData] = useState([]);
const [lastAuthor, setLastAuthor] = useState('');
const [lastPermlink, setLastPermlink] = useState('');
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [noMore, setNoMore] = useState(false);
const [data, setData] = useState([]);
const [lastAuthor, setLastAuthor] = useState('');
const [lastPermlink, setLastPermlink] = useState('');
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [noMore, setNoMore] = useState(false);
useEffect(() => {
if (selectedUser) {
_fetchData();
}
}, [selectedUser]);
useEffect(() => {
if(selectedUser){
_fetchData();
}
}, [selectedUser])
const _fetchData = async ({refresh}:{refresh?:boolean} = {}) => {
if(loading || (!refresh && noMore)){
return;
}
setLoading(true);
if(refresh){
setRefreshing(true);
}
const query:any = {
account:username,
start_author: refresh ? '' : lastAuthor,
start_permlink: refresh ? '' : lastPermlink,
limit:10,
observer:'',
sort:type
};
const result = await getAccountPosts(query)
let _comments:any[] = refresh ? result : unionBy(data, result, 'permlink');
if(Array.isArray(_comments)){
setData(_comments);
if(_comments.length > 0){
setLastAuthor(_comments[_comments.lastIndex].author)
setLastPermlink(_comments[_comments.lastIndex].permlink)
}
if(result.length == 0){
setNoMore(true);
}
}else{
setData([]);
setNoMore(true);
}
setLoading(false);
setRefreshing(false);
const _fetchData = async ({ refresh }: { refresh?: boolean } = {}) => {
if (loading || (!refresh && noMore)) {
return;
}
const _renderListEmpty = () => {
if(loading){
return null
}
return (
<NoPost
name={username}
text={intl.formatMessage({
id: 'profile.havent_commented',
})}
defaultText={intl.formatMessage({
id: 'profile.login_to_see',
})}
/>
)
setLoading(true);
if (refresh) {
setRefreshing(true);
}
const _renderListFooter = () => {
return (
<View style={styles.commentsListFooter}>
{loading && (
<ActivityIndicator size='large'/>
)}
</View>
)
const query: any = {
account: username,
start_author: refresh ? '' : lastAuthor,
start_permlink: refresh ? '' : lastPermlink,
limit: 10,
observer: '',
sort: type,
};
const result = await getAccountPosts(query);
let _comments: any[] = refresh ? result : unionBy(data, result, 'permlink');
if (Array.isArray(_comments)) {
setData(_comments);
if (_comments.length > 0) {
setLastAuthor(_comments[_comments.lastIndex].author);
setLastPermlink(_comments[_comments.lastIndex].permlink);
}
if (result.length == 0) {
setNoMore(true);
}
} else {
setData([]);
setNoMore(true);
}
setLoading(false);
setRefreshing(false);
};
const _renderListEmpty = () => {
if (loading) {
return null;
}
return (
<View key="profile.comments" style={styles.commentsTabBar}>
<Comments
comments={data}
fetchPost={()=>{}}
isOwnProfile={isOwnProfile}
isHideImage={isHideImage}
flatListProps={{
onEndReached:_fetchData,
onScroll:onScroll,
ListEmptyComponent:_renderListEmpty,
ListFooterComponent:_renderListFooter,
onEndReachedThreshold:1,
refreshControl:(
<RefreshControl
refreshing={refreshing}
onRefresh={()=>_fetchData({refresh:true})}
/>
)
}}
/>
</View>
<NoPost
name={username}
text={intl.formatMessage({
id: 'profile.havent_commented',
})}
defaultText={intl.formatMessage({
id: 'profile.login_to_see',
})}
/>
);
};
const _renderListFooter = () => {
return (
<View style={styles.commentsListFooter}>{loading && <ActivityIndicator size="large" />}</View>
);
};
return (
<View key="profile.comments" style={styles.commentsTabBar}>
<Comments
comments={data}
fetchPost={() => {}}
isOwnProfile={isOwnProfile}
isHideImage={isHideImage}
flatListProps={{
onEndReached: _fetchData,
onScroll: onScroll,
ListEmptyComponent: _renderListEmpty,
ListFooterComponent: _renderListFooter,
onEndReachedThreshold: 1,
refreshControl: (
<RefreshControl
refreshing={refreshing}
onRefresh={() => _fetchData({ refresh: true })}
/>
),
}}
/>
</View>
);
};
export default CommentsTabContent;

View File

@ -6,6 +6,8 @@ import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view
import { injectIntl } from 'react-intl';
// Images
import FastImage from 'react-native-fast-image';
import EStyleSheet from 'react-native-extended-stylesheet';
import LIGHT_COVER_IMAGE from '../../assets/default_cover_image.png';
import DARK_COVER_IMAGE from '../../assets/dark_cover_image.png';
@ -18,22 +20,19 @@ import { getResizedImage } from '../../utils/image';
// Styles
import styles from './profileEditFormStyles';
import FastImage from 'react-native-fast-image';
import EStyleSheet from 'react-native-extended-stylesheet';
import { MainButton } from '../mainButton';
interface ProfileEditFormProps {
coverUrl:string;
formData:any;
handleOnItemChange:()=>void;
handleOnSubmit:()=>void;
intl:any,
isDarkTheme:boolean,
isLoading:boolean,
isUploading:boolean,
showImageUploadActions:boolean,
saveEnabled:boolean,
coverUrl: string;
formData: any;
handleOnItemChange: () => void;
handleOnSubmit: () => void;
intl: any;
isDarkTheme: boolean;
isLoading: boolean;
isUploading: boolean;
showImageUploadActions: boolean;
saveEnabled: boolean;
}
const ProfileEditFormView = ({
@ -48,37 +47,31 @@ const ProfileEditFormView = ({
showImageUploadActions,
saveEnabled,
...props
}:ProfileEditFormProps) => (
}: ProfileEditFormProps) => (
<View style={styles.container}>
<KeyboardAwareScrollView
enableAutoAutomaticScroll={Platform.OS === 'ios'}
contentContainerStyle={styles.contentContainer}
enableOnAndroid={true}
>
<TouchableOpacity style={styles.coverImgWrapper} onPress={showImageUploadActions}>
<FastImage
style={styles.coverImg}
source={
coverUrl
? { uri: getResizedImage(coverUrl, 600) }
: isDarkTheme
? DARK_COVER_IMAGE
: LIGHT_COVER_IMAGE
}
/>
{
isUploading && (
<ActivityIndicator
style={styles.activityIndicator}
color={EStyleSheet.value('$white')}
size='large'
/>
)
<FastImage
style={styles.coverImg}
source={
coverUrl
? { uri: getResizedImage(coverUrl, 600) }
: isDarkTheme
? DARK_COVER_IMAGE
: LIGHT_COVER_IMAGE
}
/>
{isUploading && (
<ActivityIndicator
style={styles.activityIndicator}
color={EStyleSheet.value('$white')}
size="large"
/>
)}
<IconButton
iconStyle={styles.addIcon}
@ -111,11 +104,11 @@ const ProfileEditFormView = ({
</View>
))}
</KeyboardAwareScrollView>
{saveEnabled && (
<AnimatedView style={styles.floatingContainer} animation="bounceInRight">
<MainButton
style={{ width: isLoading ? null : 120, marginBottom:24, alignSelf:'flex-end' }}
style={{ width: isLoading ? null : 120, marginBottom: 24, alignSelf: 'flex-end' }}
onPress={handleOnSubmit}
iconName="save"
iconType="MaterialIcons"
@ -125,8 +118,6 @@ const ProfileEditFormView = ({
/>
</AnimatedView>
)}
</View>
);

View File

@ -2,14 +2,14 @@ import React, { useEffect, useRef, useState } from 'react';
import { ActivityIndicator, Alert, PermissionsAndroid, Platform, Text, View } from 'react-native';
import ActionSheet from 'react-native-actions-sheet';
import EStyleSheet from 'react-native-extended-stylesheet';
import QRCodeScanner from 'react-native-qrcode-scanner';
import { useIntl } from 'react-intl';
import { check, request, PERMISSIONS, RESULTS, openSettings } from 'react-native-permissions';
import styles from './qrModalStyles';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { toggleQRModal } from '../../redux/actions/uiAction';
import QRCodeScanner from 'react-native-qrcode-scanner';
import { deepLinkParser } from '../../utils/deepLinkParser';
import { useIntl } from 'react-intl';
import { navigate } from '../../navigation/service';
import { check, request, PERMISSIONS, RESULTS, openSettings } from 'react-native-permissions';
import getWindowDimensions from '../../utils/getWindowDimensions';
export interface QRModalProps {}
@ -155,7 +155,7 @@ export const QRModal = ({}: QRModalProps) => {
/>
{isProcessing && (
<View style={styles.activityIndicatorContainer}>
<ActivityIndicator color={'white'} style={styles.activityIndicator} />
<ActivityIndicator color="white" style={styles.activityIndicator} />
</View>
)}
</View>

View File

@ -1,10 +1,19 @@
import React, { useEffect, useState, useCallback, useRef } from 'react';
import React, {
useEffect,
useState,
useCallback,
useRef,
useImperativeHandle,
forwardRef,
} from 'react';
import EStyleSheet from 'react-native-extended-stylesheet';
import styles from './quickReplyModalStyles';
import { View, Text, Alert, TouchableOpacity, Keyboard, Platform } from 'react-native';
import { useIntl } from 'react-intl';
import { IconButton, MainButton, TextButton, TextInput, UserAvatar } from '..';
import { useSelector, useDispatch } from 'react-redux';
import { get } from 'lodash';
import { postBodySummary } from '@ecency/render-helper';
import styles from './quickReplyModalStyles';
import { IconButton, MainButton, TextButton, TextInput, UserAvatar } from '..';
import { delay, generateReplyPermlink, generateRndStr } from '../../utils/editor';
import { postComment } from '../../providers/hive/dhive';
import { toastNotification } from '../../redux/actions/uiAction';
@ -14,13 +23,10 @@ import {
updateDraftCache,
} from '../../redux/actions/cacheActions';
import { default as ROUTES } from '../../constants/routeNames';
import { get } from 'lodash';
import { navigate } from '../../navigation/service';
import { postBodySummary } from '@ecency/render-helper';
import { Draft } from '../../redux/reducers/cacheReducer';
import { RootState } from '../../redux/store/store';
import { useImperativeHandle } from 'react';
import { forwardRef } from 'react';
import { PointActivityIds } from '../../providers/ecency/ecency.types';
import { useUserActivityMutation } from '../../providers/queries/pointQueries';
@ -30,299 +36,280 @@ export interface QuickReplyModalContentProps {
onClose: () => void;
}
export const QuickReplyModalContent = forwardRef(({
selectedPost,
onClose,
}: QuickReplyModalContentProps, ref) => {
const intl = useIntl();
const dispatch = useDispatch();
const userActivityMutation = useUserActivityMutation();
export const QuickReplyModalContent = forwardRef(
({ selectedPost, onClose }: QuickReplyModalContentProps, ref) => {
const intl = useIntl();
const dispatch = useDispatch();
const userActivityMutation = useUserActivityMutation();
const inputRef = useRef(null);
const inputRef = useRef(null);
const currentAccount = useSelector((state: RootState) => state.account.currentAccount);
const pinCode = useSelector((state: RootState) => state.application.pin);
const drafts = useSelector((state: RootState) => state.cache.drafts);
const currentAccount = useSelector((state: RootState) => state.account.currentAccount);
const pinCode = useSelector((state: RootState) => state.application.pin);
const drafts = useSelector((state: RootState) => state.cache.drafts);
const [commentValue, setCommentValue] = useState('');
const [isSending, setIsSending] = useState(false);
const [commentValue, setCommentValue] = useState('');
const [isSending, setIsSending] = useState(false);
const headerText =
selectedPost &&
(selectedPost.summary || postBodySummary(selectedPost, 150, Platform.OS as any));
let parentAuthor = selectedPost ? selectedPost.author : '';
let parentPermlink = selectedPost ? selectedPost.permlink : '';
let draftId = `${currentAccount.name}/${parentAuthor}/${parentPermlink}`; //different draftId for each user acount
const headerText =
selectedPost && (selectedPost.summary || postBodySummary(selectedPost, 150, Platform.OS as any));
let parentAuthor = selectedPost ? selectedPost.author : '';
let parentPermlink = selectedPost ? selectedPost.permlink : '';
let draftId = `${currentAccount.name}/${parentAuthor}/${parentPermlink}`; //different draftId for each user acount
useImperativeHandle(ref, () => ({
handleSheetClose() {
_addQuickCommentIntoCache();
},
}));
// load quick comment value from cache
useEffect(() => {
let _value = '';
if (drafts.has(draftId) && currentAccount.name === drafts.get(draftId).author) {
const quickComment: Draft = drafts.get(draftId);
_value = quickComment.body;
}
useImperativeHandle(ref, () => ({
handleSheetClose() {
_addQuickCommentIntoCache();
},
}));
if (inputRef.current) {
inputRef.current.setNativeProps({
text: _value,
});
setCommentValue(_value);
}
}, [selectedPost]);
// add quick comment value into cache
const _addQuickCommentIntoCache = (value = commentValue) => {
const quickCommentDraftData: Draft = {
author: currentAccount.name,
body: value,
};
// load quick comment value from cache
useEffect(() => {
let _value = ''
if (drafts.has(draftId) && currentAccount.name === drafts.get(draftId).author) {
const quickComment: Draft = drafts.get(draftId);
_value = quickComment.body;
}
if (inputRef.current) {
inputRef.current.setNativeProps({
text: _value
})
setCommentValue(_value)
}
}, [selectedPost]);
// add quick comment value into cache
const _addQuickCommentIntoCache = (value = commentValue) => {
const quickCommentDraftData: Draft = {
author: currentAccount.name,
body: value
//add quick comment cache entry
dispatch(updateDraftCache(draftId, quickCommentDraftData));
};
//add quick comment cache entry
dispatch(updateDraftCache(draftId, quickCommentDraftData));
};
// handle close press
const _handleClosePress = () => {
onClose();
};
// handle close press
const _handleClosePress = () => {
onClose()
};
// navigate to post on summary press
const _handleOnSummaryPress = () => {
Keyboard.dismiss();
onClose();
navigate({
routeName: ROUTES.SCREENS.POST,
params: {
content: selectedPost,
},
key: get(selectedPost, 'permlink'),
});
};
// handle submit reply
const _submitReply = async () => {
if (!commentValue) {
return;
}
if (isSending) {
return;
}
if (currentAccount) {
setIsSending(true);
const permlink = generateReplyPermlink(selectedPost.author);
const parentAuthor = selectedPost.author;
const parentPermlink = selectedPost.permlink;
const parentTags = selectedPost.json_metadata.tags;
console.log(
currentAccount,
pinCode,
parentAuthor,
parentPermlink,
permlink,
commentValue,
parentTags,
);
const status = await postComment(
currentAccount,
pinCode,
parentAuthor,
parentPermlink,
permlink,
commentValue,
parentTags,
)
.then((response) => {
userActivityMutation.mutate({
pointsTy:PointActivityIds.COMMENT,
transactionId:response.id
})
setIsSending(false);
setCommentValue('');
if(inputRef.current){
inputRef.current.setNativeProps({
text: ''
})
}
dispatch(
toastNotification(
intl.formatMessage({
id: 'alert.success',
}),
),
);
//add comment cache entry
dispatch(
updateCommentCache(
`${parentAuthor}/${parentPermlink}`,
{
author: currentAccount.name,
permlink,
parent_author: parentAuthor,
parent_permlink: parentPermlink,
markdownBody: commentValue,
},
{
parentTags: parentTags || ['ecency'],
},
),
);
// delete quick comment draft cache if it exist
if (drafts.has(draftId)) {
dispatch(deleteDraftCacheEntry(draftId));
}
//close should alwasy be called at method end
onClose();
})
.catch((error) => {
console.log(error);
Alert.alert(
intl.formatMessage({
id: 'alert.fail',
}),
error.message || JSON.stringify(error),
);
setIsSending(false);
_addQuickCommentIntoCache(); //add comment value into cache if there is error while posting comment
});
console.log('status : ', status);
}
};
const _handleExpandBtn = async () => {
if (selectedPost) {
// navigate to post on summary press
const _handleOnSummaryPress = () => {
Keyboard.dismiss();
onClose();
await delay(50);
navigate({
routeName: ROUTES.SCREENS.EDITOR,
key: 'editor_replay',
routeName: ROUTES.SCREENS.POST,
params: {
isReply: true,
post: selectedPost,
content: selectedPost,
},
key: get(selectedPost, 'permlink'),
});
}
};
};
// handle submit reply
const _submitReply = async () => {
if (!commentValue) {
return;
}
if (isSending) {
return;
}
//REMOVED FOR TESTING, CAN BE PUT BACK IF APP STILL CRASHES
// const _deboucedCacheUpdate = useCallback(debounce(_addQuickCommentIntoCache, 500), [])
if (currentAccount) {
setIsSending(true);
const permlink = generateReplyPermlink(selectedPost.author);
const parentAuthor = selectedPost.author;
const parentPermlink = selectedPost.permlink;
const parentTags = selectedPost.json_metadata.tags;
console.log(
currentAccount,
pinCode,
parentAuthor,
parentPermlink,
permlink,
commentValue,
parentTags,
);
const status = await postComment(
currentAccount,
pinCode,
parentAuthor,
parentPermlink,
permlink,
commentValue,
parentTags,
)
.then((response) => {
userActivityMutation.mutate({
pointsTy: PointActivityIds.COMMENT,
transactionId: response.id,
});
setIsSending(false);
setCommentValue('');
if (inputRef.current) {
inputRef.current.setNativeProps({
text: '',
});
}
dispatch(
toastNotification(
intl.formatMessage({
id: 'alert.success',
}),
),
);
//add comment cache entry
dispatch(
updateCommentCache(
`${parentAuthor}/${parentPermlink}`,
{
author: currentAccount.name,
permlink,
parent_author: parentAuthor,
parent_permlink: parentPermlink,
markdownBody: commentValue,
},
{
parentTags: parentTags || ['ecency'],
},
),
);
// delete quick comment draft cache if it exist
if (drafts.has(draftId)) {
dispatch(deleteDraftCacheEntry(draftId));
}
//close should alwasy be called at method end
onClose();
})
.catch((error) => {
console.log(error);
Alert.alert(
intl.formatMessage({
id: 'alert.fail',
}),
error.message || JSON.stringify(error),
);
setIsSending(false);
_addQuickCommentIntoCache(); //add comment value into cache if there is error while posting comment
});
console.log('status : ', status);
}
};
const _handleExpandBtn = async () => {
if (selectedPost) {
Keyboard.dismiss();
onClose();
await delay(50);
navigate({
routeName: ROUTES.SCREENS.EDITOR,
key: 'editor_replay',
params: {
isReply: true,
post: selectedPost,
},
});
}
};
const _onChangeText = (value) => {
setCommentValue(value);
//REMOVED FOR TESTING, CAN BE PUT BACK IF APP STILL CRASHES
// _deboucedCacheUpdate(value)
}
// const _deboucedCacheUpdate = useCallback(debounce(_addQuickCommentIntoCache, 500), [])
const _onChangeText = (value) => {
setCommentValue(value);
//REMOVED FOR TESTING, CAN BE PUT BACK IF APP STILL CRASHES
// _deboucedCacheUpdate(value)
};
//VIEW_RENDERERS
//VIEW_RENDERERS
const _renderSummary = () => (
<TouchableOpacity onPress={() => _handleOnSummaryPress()}>
<Text numberOfLines={2} style={styles.summaryStyle}>
{headerText}
</Text>
</TouchableOpacity>
);
const _renderSummary = () => (
<TouchableOpacity onPress={() => _handleOnSummaryPress()}>
<Text numberOfLines={2} style={styles.summaryStyle}>
{headerText}
</Text>
</TouchableOpacity>
);
const _renderAvatar = () => (
<View style={styles.avatarAndNameContainer}>
<UserAvatar noAction username={currentAccount.username} />
<View style={styles.nameContainer}>
<Text style={styles.name}>{`@${currentAccount.username}`}</Text>
const _renderAvatar = () => (
<View style={styles.avatarAndNameContainer}>
<UserAvatar noAction username={currentAccount.username} />
<View style={styles.nameContainer}>
<Text style={styles.name}>{`@${currentAccount.username}`}</Text>
</View>
</View>
</View>
);
);
const _renderExpandBtn = () => (
<View style={styles.expandBtnContainer}>
<IconButton
iconStyle={styles.backIcon}
iconType="MaterialCommunityIcons"
name="arrow-expand"
onPress={_handleExpandBtn}
size={28}
color={EStyleSheet.value('$primaryBlack')}
/>
</View>
);
const _renderReplyBtn = () => (
<View style={styles.replyBtnContainer}>
<TextButton
style={styles.cancelButton}
onPress={_handleClosePress}
text={intl.formatMessage({
id: 'quick_reply.close',
})}
/>
<MainButton
style={styles.commentBtn}
onPress={() => _submitReply()}
text={intl.formatMessage({
id: 'quick_reply.reply',
})}
isLoading={isSending}
/>
</View>
);
const _renderContent = (
<View style={styles.modalContainer}>
{_renderSummary()}
{_renderAvatar()}
<View style={styles.inputContainer}>
<TextInput
innerRef={inputRef}
onChangeText={_onChangeText}
autoFocus
placeholder={intl.formatMessage({
id: 'quick_reply.placeholder',
})}
placeholderTextColor="#c1c5c7"
style={styles.textInput}
multiline={true}
numberOfLines={5}
textAlignVertical="top"
const _renderExpandBtn = () => (
<View style={styles.expandBtnContainer}>
<IconButton
iconStyle={styles.backIcon}
iconType="MaterialCommunityIcons"
name="arrow-expand"
onPress={_handleExpandBtn}
size={28}
color={EStyleSheet.value('$primaryBlack')}
/>
</View>
<View style={styles.footer}>
{_renderExpandBtn()}
{_renderReplyBtn()}
);
const _renderReplyBtn = () => (
<View style={styles.replyBtnContainer}>
<TextButton
style={styles.cancelButton}
onPress={_handleClosePress}
text={intl.formatMessage({
id: 'quick_reply.close',
})}
/>
<MainButton
style={styles.commentBtn}
onPress={() => _submitReply()}
text={intl.formatMessage({
id: 'quick_reply.reply',
})}
isLoading={isSending}
/>
</View>
</View>
)
);
return _renderContent
});
const _renderContent = (
<View style={styles.modalContainer}>
{_renderSummary()}
{_renderAvatar()}
<View style={styles.inputContainer}>
<TextInput
innerRef={inputRef}
onChangeText={_onChangeText}
autoFocus
placeholder={intl.formatMessage({
id: 'quick_reply.placeholder',
})}
placeholderTextColor="#c1c5c7"
style={styles.textInput}
multiline={true}
numberOfLines={5}
textAlignVertical="top"
/>
</View>
<View style={styles.footer}>
{_renderExpandBtn()}
{_renderReplyBtn()}
</View>
</View>
);
return _renderContent;
},
);

View File

@ -3,18 +3,18 @@ import EStyleSheet from 'react-native-extended-stylesheet';
export default EStyleSheet.create({
sheetContent: {
backgroundColor: '$primaryBackgroundColor',
marginTop:132,
marginTop: 132,
},
container: {
flex: 1,
justifyContent:"flex-end",
justifyContent: 'flex-end',
backgroundColor: 'rgba(0, 0, 0, 0.2)',
},
modalContainer: {
margin:16,
borderRadius:16,
margin: 16,
borderRadius: 16,
backgroundColor: '$primaryBackgroundColor',
paddingTop: 16,
paddingBottom: 16,
@ -44,7 +44,7 @@ export default EStyleSheet.create({
},
textInput: {
color: '$primaryBlack',
paddingHorizontal:16,
paddingHorizontal: 16,
fontSize: 16,
flexGrow: 1,
fontWeight: '500',

View File

@ -5,34 +5,27 @@ import { useAppDispatch, useAppSelector } from '../../hooks';
import { hideReplyModal } from '../../redux/actions/uiAction';
const QuickReplyModal = () => {
const dispatch = useAppDispatch();
const replyModalVisible = useAppSelector((state) => state.ui.replyModalVisible);
const replyModalPost = useAppSelector(state => state.ui.replyModalPost)
const replyModalPost = useAppSelector((state) => state.ui.replyModalPost);
const modalContentRef = useRef(null);
const _onClose = () => {
if(modalContentRef.current){
if (modalContentRef.current) {
modalContentRef.current.handleSheetClose();
}
dispatch(hideReplyModal());
}
};
return (
<InputSupportModal
visible={replyModalVisible && !!replyModalPost}
onClose={_onClose}
>
<InputSupportModal visible={replyModalVisible && !!replyModalPost} onClose={_onClose}>
<QuickReplyModalContent
ref={modalContentRef}
selectedPost={replyModalPost}
onClose={_onClose}
/>
</InputSupportModal>
);
};

View File

@ -1 +1 @@
export * from './simpleChart';
export * from './simpleChart';

View File

@ -1,50 +1,54 @@
import React from 'react'
import { LineChart } from 'react-native-chart-kit'
import React from 'react';
import { LineChart } from 'react-native-chart-kit';
import EStyleSheet from 'react-native-extended-stylesheet';
interface CoinChartProps {
data:number[],
baseWidth:number,
chartHeight:number,
showLine:boolean,
showLabels?:boolean,
data: number[];
baseWidth: number;
chartHeight: number;
showLine: boolean;
showLabels?: boolean;
}
export const SimpleChart = ({data, baseWidth, chartHeight, showLine, showLabels = false}:CoinChartProps) => {
if(!data || !data.length){
return null;
}
const _chartWidth = baseWidth + baseWidth/(data.length -1)
const _chartBackgroundColor = EStyleSheet.value('$primaryLightBackground');
return (
<LineChart
data={{
labels: [],
datasets: [
{
data
}
],
}}
width={_chartWidth} // from react-native
height={chartHeight}
withHorizontalLabels={showLabels}
withVerticalLabels={false}
withHorizontalLines={false}
withDots={false}
withInnerLines={false}
chartConfig={{
backgroundColor:_chartBackgroundColor,
backgroundGradientFrom: _chartBackgroundColor,
backgroundGradientTo: _chartBackgroundColor,
fillShadowGradient: EStyleSheet.value('$chartBlue'),
fillShadowGradientOpacity:0.8,
labelColor:() => EStyleSheet.value('$primaryDarkText'),
color: () => showLine?EStyleSheet.value('$chartBlue'):'transparent',
}}
/>
)
}
export const SimpleChart = ({
data,
baseWidth,
chartHeight,
showLine,
showLabels = false,
}: CoinChartProps) => {
if (!data || !data.length) {
return null;
}
const _chartWidth = baseWidth + baseWidth / (data.length - 1);
const _chartBackgroundColor = EStyleSheet.value('$primaryLightBackground');
return (
<LineChart
data={{
labels: [],
datasets: [
{
data,
},
],
}}
width={_chartWidth} // from react-native
height={chartHeight}
withHorizontalLabels={showLabels}
withVerticalLabels={false}
withHorizontalLines={false}
withDots={false}
withInnerLines={false}
chartConfig={{
backgroundColor: _chartBackgroundColor,
backgroundGradientFrom: _chartBackgroundColor,
backgroundGradientTo: _chartBackgroundColor,
fillShadowGradient: EStyleSheet.value('$chartBlue'),
fillShadowGradientOpacity: 0.8,
labelColor: () => EStyleSheet.value('$primaryDarkText'),
color: () => (showLine ? EStyleSheet.value('$chartBlue') : 'transparent'),
}}
/>
);
};

View File

@ -1,192 +1,192 @@
import { getAccountPosts, getPost, getRankedPosts } from "../../../providers/hive/dhive";
import { filterLatestPosts, getUpdatedPosts } from "./tabbedPostsHelpers";
import { LoadPostsOptions } from "./tabbedPostsModels";
import { getPromotedEntries } from "../../../providers/ecency/ecency";
import { getAccountPosts, getPost, getRankedPosts } from '../../../providers/hive/dhive';
import { filterLatestPosts, getUpdatedPosts } from './tabbedPostsHelpers';
import { LoadPostsOptions } from './tabbedPostsModels';
import { getPromotedEntries } from '../../../providers/ecency/ecency';
const POSTS_FETCH_COUNT = 20;
export const loadPosts = async ({
filterKey,
prevPosts,
tabMeta,
setTabMeta,
isLatestPostsCheck = false,
getFor,
isConnected,
isLoggedIn,
refreshing,
feedUsername,
pinnedPermlink,
pageType,
tag,
nsfw,
filterKey,
prevPosts,
tabMeta,
setTabMeta,
isLatestPostsCheck = false,
getFor,
isConnected,
isLoggedIn,
refreshing,
feedUsername,
pinnedPermlink,
pageType,
tag,
nsfw,
}: LoadPostsOptions) => {
let filter = filterKey;
}:LoadPostsOptions) => {
let filter = filterKey;
//match filter with api if is friends
if(filter === 'friends'){
filter = 'feed';
}
//match filter with api if is friends
if (filter === 'friends') {
filter = 'feed';
}
const {isLoading, startPermlink, startAuthor} = tabMeta;
//reject update if already loading
if (
isLoading ||
!isConnected ||
(!isLoggedIn && filterKey === 'feed') ||
(!isLoggedIn && filterKey === 'communities')
) {
return;
}
const { isLoading, startPermlink, startAuthor } = tabMeta;
//reject update if no connection
if (!isConnected && (refreshing || isLoading)) {
setTabMeta({
...tabMeta,
isLoading:false,
isRefreshing:false,
})
return;
}
//reject update if already loading
if (
isLoading ||
!isConnected ||
(!isLoggedIn && filterKey === 'feed') ||
(!isLoggedIn && filterKey === 'communities')
) {
return;
}
//reject update if no connection
if (!isConnected && (refreshing || isLoading)) {
setTabMeta({
...tabMeta,
isLoading:true,
isRefreshing:refreshing,
})
let options = {} as any;
const limit = isLatestPostsCheck ? 5 : POSTS_FETCH_COUNT;
let func = null;
isLoading: false,
isRefreshing: false,
});
return;
}
if (
filter === 'feed' ||
filter === 'communities' ||
filter === 'posts' ||
filter === 'blog' ||
getFor === 'blog' ||
filter === 'reblogs'
) {
if (filter === 'communities') {
func = getRankedPosts;
options = {
observer: feedUsername,
sort: 'created',
tag: 'my',
limit,
};
} else {
func = getAccountPosts;
options = {
observer: feedUsername || '',
account: feedUsername,
limit,
sort: filter,
};
setTabMeta({
...tabMeta,
isLoading: true,
isRefreshing: refreshing,
});
if ((pageType === 'profile' || pageType === 'ownProfile') && (filter === 'feed' || filter === 'posts')) {
options.sort = 'posts';
}
}
} else {
let options = {} as any;
const limit = isLatestPostsCheck ? 5 : POSTS_FETCH_COUNT;
let func = null;
if (
filter === 'feed' ||
filter === 'communities' ||
filter === 'posts' ||
filter === 'blog' ||
getFor === 'blog' ||
filter === 'reblogs'
) {
if (filter === 'communities') {
func = getRankedPosts;
options = {
tag,
observer: feedUsername,
sort: 'created',
tag: 'my',
limit,
};
} 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,
};
}
if (startAuthor && startPermlink && !refreshing && !isLatestPostsCheck) {
options.start_author = startAuthor;
options.start_permlink = startPermlink;
}
if (startAuthor && startPermlink && !refreshing && !isLatestPostsCheck) {
options.start_author = startAuthor;
options.start_permlink = startPermlink;
}
try {
//fetching posts
const result: any[] = await func(options, feedUsername, nsfw);
try {
//fetching posts
const result:any[] = await func(options, feedUsername, nsfw);
if(result.length > 0){
if(filter === 'reblogs'){
for (let i = result.length - 1; i >= 0; i--) {
if (result[i].author === feedUsername) {
result.splice(i, 1);
}
if (result.length > 0) {
if (filter === 'reblogs') {
for (let i = result.length - 1; i >= 0; i--) {
if (result[i].author === feedUsername) {
result.splice(i, 1);
}
}
if((pageType === 'profile' || pageType === 'ownProfile') && pinnedPermlink){
let pinnedIndex = -1;
result.forEach((post, index)=>{
if(post.author === feedUsername && post.permlink === pinnedPermlink){
pinnedIndex = index;
}
})
result.splice(pinnedIndex, 1);
}
}
//if filter is feed convert back to reducer filter
if(filter === 'feed'){
filter = 'friends'
if ((pageType === 'profile' || pageType === 'ownProfile') && pinnedPermlink) {
let pinnedIndex = -1;
result.forEach((post, index) => {
if (post.author === feedUsername && post.permlink === pinnedPermlink) {
pinnedIndex = index;
}
});
result.splice(pinnedIndex, 1);
}
// cacheDispatch(updateFilterCache(filter, result, refreshing))
setTabMeta({
...tabMeta,
isLoading:false,
isRefreshing:false,
})
const retData = {
latestPosts:null,
updatedPosts:null
}
if(isLatestPostsCheck){
const latestPosts = filterLatestPosts(result, prevPosts.slice(0, 5));
retData.latestPosts = latestPosts
}else{
const updatedPosts = getUpdatedPosts(
startAuthor && startPermlink ? prevPosts:[],
result,
refreshing,
tabMeta,
setTabMeta,
)
retData.updatedPosts = updatedPosts;
}
//fetch add pinned posts if applicable
if(retData.updatedPosts && pinnedPermlink && retData.updatedPosts[0].permlink !== pinnedPermlink){
const pinnedPost = await getPost(feedUsername, pinnedPermlink);
pinnedPost.stats = {is_pinned_blog:true, ...pinnedPost.stats};
retData.updatedPosts = [pinnedPost, ...retData.updatedPosts];
}
return retData
} catch (err) {
setTabMeta({
...tabMeta,
isLoading:false,
isRefreshing:false,
})
}
};
export const fetchPromotedEntries = async (username:string) => {
try {
const posts = await getPromotedEntries(username);
return Array.isArray(posts) ? posts : [];
} catch(err){
console.warn("Failed to get promoted posts, ", err)
//if filter is feed convert back to reducer filter
if (filter === 'feed') {
filter = 'friends';
}
}
// cacheDispatch(updateFilterCache(filter, result, refreshing))
setTabMeta({
...tabMeta,
isLoading: false,
isRefreshing: false,
});
const retData = {
latestPosts: null,
updatedPosts: null,
};
if (isLatestPostsCheck) {
const latestPosts = filterLatestPosts(result, prevPosts.slice(0, 5));
retData.latestPosts = latestPosts;
} else {
const updatedPosts = getUpdatedPosts(
startAuthor && startPermlink ? prevPosts : [],
result,
refreshing,
tabMeta,
setTabMeta,
);
retData.updatedPosts = updatedPosts;
}
//fetch add pinned posts if applicable
if (
retData.updatedPosts &&
pinnedPermlink &&
retData.updatedPosts[0].permlink !== pinnedPermlink
) {
const pinnedPost = await getPost(feedUsername, pinnedPermlink);
pinnedPost.stats = { is_pinned_blog: true, ...pinnedPost.stats };
retData.updatedPosts = [pinnedPost, ...retData.updatedPosts];
}
return retData;
} catch (err) {
setTabMeta({
...tabMeta,
isLoading: false,
isRefreshing: false,
});
}
};
export const fetchPromotedEntries = async (username: string) => {
try {
const posts = await getPromotedEntries(username);
return Array.isArray(posts) ? posts : [];
} catch (err) {
console.warn('Failed to get promoted posts, ', err);
}
};

View File

@ -1,7 +1,5 @@
import unionBy from 'lodash/unionBy';
import { TabMeta } from "./tabbedPostsModels";
import { TabMeta } from './tabbedPostsModels';
//cacludate posts check refresh time for selected filter;
export const calculateTimeLeftForPostCheck = (firstPost: any) => {
@ -17,58 +15,50 @@ export const calculateTimeLeftForPostCheck = (firstPost: any) => {
timeLeft = refetchTime;
}
return timeLeft;
}
};
//filter posts that are not present in top 5 posts currently in list.
export const filterLatestPosts = (fetchedPosts: any[], cachedPosts: any[]) => {
console.log("Comparing: ", fetchedPosts, cachedPosts);
console.log('Comparing: ', fetchedPosts, cachedPosts);
let latestPosts = [];
fetchedPosts.forEach((post) => {
const newPostAuthPrem = post.author + post.permlink;
const postExist = cachedPosts.find((cPost) => (cPost.author + post.permlink) === newPostAuthPrem);
const postExist = cachedPosts.find((cPost) => cPost.author + post.permlink === newPostAuthPrem);
if (!postExist) {
latestPosts.push(post);
}
});
if (latestPosts.length > 0) {
return latestPosts.slice(0, 5);
} else {
return [];
}
};
//process posts result and return updated posts for the list.
export const getUpdatedPosts = (
prevPosts: any[],
nextPosts: any[],
shouldReset: boolean,
tabMeta: TabMeta,
setTabMeta: (meta: TabMeta) => void
setTabMeta: (meta: TabMeta) => void,
) => {
//return state as is if component is unmounter
let _posts = nextPosts;
if (nextPosts.length === 0) {
setTabMeta({
...tabMeta,
isNoPost: true
isNoPost: true,
});
return shouldReset ? [] : prevPosts;
}
const refreshing = tabMeta.isRefreshing;
if (prevPosts.length > 0 && !shouldReset) {
if (refreshing) {
_posts = unionBy(_posts, prevPosts, 'permlink');
@ -81,8 +71,7 @@ export const getUpdatedPosts = (
...tabMeta,
startAuthor: _posts[_posts.length - 1] && _posts[_posts.length - 1].author,
startPermlink: _posts[_posts.length - 1] && _posts[_posts.length - 1].permlink,
})
return _posts
}
});
return _posts;
};

View File

@ -1,63 +1,59 @@
export interface TabbedPostsProps {
filterOptions:string[],
filterOptionsValue:string[],
isFeedScreen:boolean,
feedUsername:string,
selectedOptionIndex:number,
feedSubfilterOptions:string[],
feedSubfilterOptionsValue:string[],
getFor:string,
pageType:'main'|'community'|'profile'|'ownProfile',
tag:string,
forceLoadPosts:boolean,
tabContentOverrides:Map<number, any>,
imagesToggleEnabled?:boolean,
stackedTabs:boolean,
pinnedPermlink?:string,
onTabChange:(index:number)=>void
handleOnScroll:()=>void,
filterOptions: string[];
filterOptionsValue: string[];
isFeedScreen: boolean;
feedUsername: string;
selectedOptionIndex: number;
feedSubfilterOptions: string[];
feedSubfilterOptionsValue: string[];
getFor: string;
pageType: 'main' | 'community' | 'profile' | 'ownProfile';
tag: string;
forceLoadPosts: boolean;
tabContentOverrides: Map<number, any>;
imagesToggleEnabled?: boolean;
stackedTabs: boolean;
pinnedPermlink?: string;
onTabChange: (index: number) => void;
handleOnScroll: () => void;
}
export interface TabMeta {
startPermlink:string,
startAuthor:string,
isLoading:boolean,
isRefreshing:boolean,
isNoPost:boolean,
}
export interface LoadPostsOptions {
filterKey:string;
prevPosts:any[];
tabMeta:TabMeta;
setTabMeta:(meta:TabMeta)=>void,
getFor:string,
isConnected:boolean,
isLoggedIn:boolean,
feedUsername:string,
pinnedPermlink:string,
pageType:string,
tag:string,
nsfw:string,
isLatestPostsCheck?:boolean,
refreshing?:boolean,
startPermlink: string;
startAuthor: string;
isLoading: boolean;
isRefreshing: boolean;
isNoPost: boolean;
}
}
export interface LoadPostsOptions {
filterKey: string;
prevPosts: any[];
tabMeta: TabMeta;
setTabMeta: (meta: TabMeta) => void;
getFor: string;
isConnected: boolean;
isLoggedIn: boolean;
feedUsername: string;
pinnedPermlink: string;
pageType: string;
tag: string;
nsfw: string;
isLatestPostsCheck?: boolean;
refreshing?: boolean;
}
export interface TabContentProps {
filterKey:string,
isFeedScreen:boolean,
isInitialTab:boolean,
getFor:string,
pageType:'main'|'profile'|'ownProfile'|'community',
feedUsername:string,
tag:string,
forceLoadPosts:boolean,
filterScrollRequest:string,
pinnedPermlink?:string,
onScrollRequestProcessed:()=>void
handleOnScroll:()=>void;
}
export interface TabContentProps {
filterKey: string;
isFeedScreen: boolean;
isInitialTab: boolean;
getFor: string;
pageType: 'main' | 'profile' | 'ownProfile' | 'community';
feedUsername: string;
tag: string;
forceLoadPosts: boolean;
filterScrollRequest: string;
pinnedPermlink?: string;
onScrollRequestProcessed: () => void;
handleOnScroll: () => void;
}

View File

@ -1,30 +1,29 @@
import React, {useEffect, useState} from 'react';
import React, { useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
import { get } from 'lodash';
import { Text, View, FlatList } from 'react-native';
import { withNavigation } from '@react-navigation/compat';
import { useSelector, useDispatch } from 'react-redux';
import { NoPost, PostCardPlaceHolder, UserListItem } from '../..';
import globalStyles from '../../../globalStyles';
import { CommunityListItem, EmptyScreen } from '../../basicUIElements';
import styles from './tabbedPostsStyles';
import { default as ROUTES } from '../../../constants/routeNames';
import { withNavigation } from '@react-navigation/compat';
import {useSelector, useDispatch } from 'react-redux';
import { fetchCommunities, leaveCommunity, subscribeCommunity } from '../../../redux/actions/communitiesAction';
import {
fetchCommunities,
leaveCommunity,
subscribeCommunity,
} from '../../../redux/actions/communitiesAction';
import { fetchLeaderboard, followUser, unfollowUser } from '../../../redux/actions/userAction';
import { getCommunity } from '../../../providers/hive/dhive';
interface TabEmptyViewProps {
filterKey:string,
isNoPost:boolean,
navigation:any,
filterKey: string;
isNoPost: boolean;
navigation: any;
}
const TabEmptyView = ({
filterKey,
isNoPost,
navigation,
}: TabEmptyViewProps) => {
const TabEmptyView = ({ filterKey, isNoPost, navigation }: TabEmptyViewProps) => {
const intl = useIntl();
const dispatch = useDispatch();
//redux properties
@ -43,23 +42,22 @@ const TabEmptyView = ({
//hooks
useEffect(()=>{
useEffect(() => {
if (isNoPost) {
if (filterKey === 'friends') {
if (recommendedUsers.length === 0) {
_getRecommendedUsers();
}
} else if(filterKey === 'communities') {
} else if (filterKey === 'communities') {
if (recommendedCommunities.length === 0) {
_getRecommendedCommunities();
}
}
}
}, [isNoPost])
}, [isNoPost]);
useEffect(() => {
const {loading, error, data} = leaderboard;
const { loading, error, data } = leaderboard;
if (!loading) {
if (!error && data && data.length > 0) {
_formatRecommendedUsers(data);
@ -68,7 +66,7 @@ const TabEmptyView = ({
}, [leaderboard]);
useEffect(() => {
const {loading, error, data} = communities;
const { loading, error, data } = communities;
if (!loading) {
if (!error && data && data?.length > 0) {
_formatRecommendedCommunities(data);
@ -76,7 +74,6 @@ const TabEmptyView = ({
}
}, [communities]);
useEffect(() => {
const recommendeds = [...recommendedCommunities];
@ -103,8 +100,6 @@ const TabEmptyView = ({
setRecommendedCommunities(recommendeds);
}, [subscribingCommunities]);
useEffect(() => {
const recommendeds = [...recommendedUsers];
@ -130,13 +125,12 @@ const TabEmptyView = ({
setRecommendedUsers(recommendeds);
}, [followingUsers]);
//fetching
const _getRecommendedUsers = () => dispatch(fetchLeaderboard());
const _getRecommendedCommunities = () => dispatch(fetchCommunities('', 10));
//formating
//formating
const _formatRecommendedCommunities = async (communitiesArray) => {
try {
const ecency = await getCommunity('hive-125125');
@ -192,7 +186,6 @@ const TabEmptyView = ({
);
};
const _handleFollowUserButtonPress = (data, isFollowing) => {
let followAction;
let successToastText = '';
@ -223,14 +216,15 @@ const TabEmptyView = ({
dispatch(followAction(currentAccount, pinCode, data, successToastText, failToastText));
};
const _handleOnPressLogin = () => {
navigation.navigate(ROUTES.SCREENS.LOGIN);
};
//render related operations
if ((filterKey === 'feed' || filterKey === 'friends' || filterKey === 'communities') && !isLoggedIn) {
//render related operations
if (
(filterKey === 'feed' || filterKey === 'friends' || filterKey === 'communities') &&
!isLoggedIn
) {
return (
<NoPost
imageStyle={styles.noImage}
@ -245,92 +239,89 @@ const TabEmptyView = ({
if (isNoPost) {
if (filterKey === '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' })
}
rightTextStyle={[styles.followText, item.isFollowing && styles.unfollowText]}
isLoggedIn={isLoggedIn}
isFollowing={item.isFollowing}
isLoadingRightAction={
followingUsers.hasOwnProperty(item._id) && followingUsers[item._id].loading
}
onPressRightText={_handleFollowUserButtonPress}
handleOnPress={(username) =>
navigation.navigate({
routeName: ROUTES.SCREENS.PROFILE,
params: {
username,
},
key: username,
})
}
/>
)}
/>
</>
);
} else if (filterKey === 'communities') {
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({
routeName: ROUTES.SCREENS.COMMUNITY,
params: {
tag: name,
},
})
}
handleSubscribeButtonPress={_handleSubscribeCommunityButtonPress}
isSubscribed={item.isSubscribed}
isLoadingRightAction={
subscribingCommunities.hasOwnProperty(item.name) &&
subscribingCommunities[item.name].loading
}
isLoggedIn={isLoggedIn}
/>
)}
/>
</>
);
} else {
return (
<EmptyScreen style={styles.emptyAnimationContainer} />
)
<>
<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' })
}
rightTextStyle={[styles.followText, item.isFollowing && styles.unfollowText]}
isLoggedIn={isLoggedIn}
isFollowing={item.isFollowing}
isLoadingRightAction={
followingUsers.hasOwnProperty(item._id) && followingUsers[item._id].loading
}
onPressRightText={_handleFollowUserButtonPress}
handleOnPress={(username) =>
navigation.navigate({
routeName: ROUTES.SCREENS.PROFILE,
params: {
username,
},
key: username,
})
}
/>
)}
/>
</>
);
} else if (filterKey === 'communities') {
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({
routeName: ROUTES.SCREENS.COMMUNITY,
params: {
tag: name,
},
})
}
handleSubscribeButtonPress={_handleSubscribeCommunityButtonPress}
isSubscribed={item.isSubscribed}
isLoadingRightAction={
subscribingCommunities.hasOwnProperty(item.name) &&
subscribingCommunities[item.name].loading
}
isLoggedIn={isLoggedIn}
/>
)}
/>
</>
);
} else {
return <EmptyScreen style={styles.emptyAnimationContainer} />;
}
}
@ -342,4 +333,3 @@ const TabEmptyView = ({
};
export default withNavigation(TabEmptyView);

View File

@ -1,119 +1,105 @@
import React, { useRef, useState } from "react";
import { useIntl } from "react-intl";
import { useDispatch, useSelector } from "react-redux";
import { CustomiseFiltersModal, FilterBar } from "../..";
import { setHidePostsThumbnails } from "../../../redux/actions/applicationActions";
import { CustomiseFiltersModalRef } from "../../customiseFiltersModal/customiseFiltersModal";
import React, { useRef, useState } from 'react';
import { useIntl } from 'react-intl';
import { useDispatch, useSelector } from 'react-redux';
import { CustomiseFiltersModal, FilterBar } from '../..';
import { setHidePostsThumbnails } from '../../../redux/actions/applicationActions';
import { CustomiseFiltersModalRef } from '../../customiseFiltersModal/customiseFiltersModal';
export interface TabItem {
filterKey:string;
label:string;
filterKey: string;
label: string;
}
interface StackedTabBarProps {
activeTab:boolean;
goToPage:(pageIndex)=>void;
tabs:string[];
pageType?:'main'|'community'|'profile'|'ownProfile'
shouldStack:boolean;
firstStack:TabItem[];
secondStack:TabItem[];
initialFirstStackIndex:number;
onFilterSelect:(filterKey:string)=>void;
toggleHideImagesFlag:boolean;
activeTab: boolean;
goToPage: (pageIndex) => void;
tabs: string[];
pageType?: 'main' | 'community' | 'profile' | 'ownProfile';
shouldStack: boolean;
firstStack: TabItem[];
secondStack: TabItem[];
initialFirstStackIndex: number;
onFilterSelect: (filterKey: string) => void;
toggleHideImagesFlag: boolean;
}
export const StackedTabBar = ({
goToPage,
tabs,
shouldStack,
firstStack,
secondStack,
initialFirstStackIndex,
onFilterSelect,
toggleHideImagesFlag,
pageType
goToPage,
tabs,
shouldStack,
firstStack,
secondStack,
initialFirstStackIndex,
onFilterSelect,
toggleHideImagesFlag,
pageType,
}: StackedTabBarProps) => {
const dispatch = useDispatch();
const intl = useIntl();
}:StackedTabBarProps) => {
const customiseModalRef = useRef<CustomiseFiltersModalRef>();
const dispatch = useDispatch();
const intl = useIntl();
//redux properties
const isHideImages = useSelector((state) => state.application.hidePostsThumbnails);
const customiseModalRef = useRef<CustomiseFiltersModalRef>();
//redux properties
const isHideImages = useSelector((state) => state.application.hidePostsThumbnails);
const [selectedFilterIndex, setSelectedFilterIndex] = useState(initialFirstStackIndex);
const [selectedSecondStackIndex, setSelectedSecondStackIndex] = useState(0);
const [selectedFilterIndex, setSelectedFilterIndex] = useState(initialFirstStackIndex);
const [selectedSecondStackIndex, setSelectedSecondStackIndex] = useState(0);
const enableCustomTabs = pageType !== undefined;
const enableCustomTabs = pageType !== undefined;
const _onCustomisePress = () => {
if(customiseModalRef.current){
customiseModalRef.current.show();
}
const _onCustomisePress = () => {
if (customiseModalRef.current) {
customiseModalRef.current.show();
}
};
const _onToggleImagesPress = () => {
dispatch(setHidePostsThumbnails(!isHideImages))
}
const _onToggleImagesPress = () => {
dispatch(setHidePostsThumbnails(!isHideImages));
};
return (
<>
return (
<>
<FilterBar
options={firstStack.map((item, index) => {
return tabs[index]
? tabs[index]
: intl.formatMessage({ id: item.label.toLowerCase() }).toUpperCase()
})
}
return tabs[index]
? tabs[index]
: intl.formatMessage({ id: item.label.toLowerCase() }).toUpperCase();
})}
selectedOptionIndex={selectedFilterIndex}
rightIconName={toggleHideImagesFlag && "view-module"}
rightIconType={toggleHideImagesFlag && "MaterialIcons"}
rightIconName={toggleHideImagesFlag && 'view-module'}
rightIconType={toggleHideImagesFlag && 'MaterialIcons'}
enableCustomiseButton={enableCustomTabs}
onCustomisePress={_onCustomisePress}
onDropdownSelect={(index)=>{
onDropdownSelect={(index) => {
setSelectedFilterIndex(index);
if(index == 0 && shouldStack){
if (index == 0 && shouldStack) {
const tabIndex = firstStack.length + selectedSecondStackIndex;
onFilterSelect(secondStack[selectedSecondStackIndex].filterKey);
goToPage(tabIndex)
}else{
goToPage(tabIndex);
} else {
onFilterSelect(firstStack[index].filterKey);
goToPage(index);
}
}}
onRightIconPress={_onToggleImagesPress}
/>
{
selectedFilterIndex == 0 && shouldStack && (
<FilterBar
options={secondStack.map((item) =>
intl.formatMessage({ id: item.label.toLowerCase() }).toUpperCase(),
)}
selectedOptionIndex={selectedSecondStackIndex}
onDropdownSelect={(index)=>{
setSelectedSecondStackIndex(index)
onFilterSelect(secondStack[index].filterKey);
goToPage(firstStack.length + index);
}}
/>
)
}
{enableCustomTabs && (
<CustomiseFiltersModal
pageType={pageType}
ref={customiseModalRef}
{selectedFilterIndex == 0 && shouldStack && (
<FilterBar
options={secondStack.map((item) =>
intl.formatMessage({ id: item.label.toLowerCase() }).toUpperCase(),
)}
selectedOptionIndex={selectedSecondStackIndex}
onDropdownSelect={(index) => {
setSelectedSecondStackIndex(index);
onFilterSelect(secondStack[index].filterKey);
goToPage(firstStack.length + index);
}}
/>
)}
</>
)
}
{enableCustomTabs && <CustomiseFiltersModal pageType={pageType} ref={customiseModalRef} />}
</>
);
};

View File

@ -1,30 +1,30 @@
import React, {useState, useEffect, useRef} from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { AppState, NativeScrollEvent, NativeSyntheticEvent } from 'react-native';
import { debounce } from 'lodash';
import PostsList from '../../postsList';
import { fetchPromotedEntries, loadPosts } from '../services/tabbedPostsFetch';
import { LoadPostsOptions, TabContentProps, TabMeta } from '../services/tabbedPostsModels';
import {useSelector, useDispatch } from 'react-redux';
import TabEmptyView from './listEmptyView';
import { setInitPosts } from '../../../redux/actions/postsAction';
import { showReplyModal } from '../../../redux/actions/uiAction';
import { calculateTimeLeftForPostCheck } from '../services/tabbedPostsHelpers';
import { AppState, NativeScrollEvent, NativeSyntheticEvent } from 'react-native';
import { PostsListRef } from '../../postsList/container/postsListContainer';
import ScrollTopPopup from './scrollTopPopup';
import { debounce } from 'lodash';
const DEFAULT_TAB_META = {
startAuthor:'',
startPermlink:'',
isLoading:false,
isRefreshing:false,
} as TabMeta;
startAuthor: '',
startPermlink: '',
isLoading: false,
isRefreshing: false,
} as TabMeta;
var scrollOffset = 0;
var blockPopup = false;
const SCROLL_POPUP_THRESHOLD = 5000;
const TabContent = ({
filterKey,
filterKey,
isFeedScreen,
isInitialTab,
pageType,
@ -39,31 +39,29 @@ const TabContent = ({
}: TabContentProps) => {
let _isMounted = true;
//redux properties
const dispatch = useDispatch();
const isLoggedIn = useSelector((state) => state.application.isLoggedIn);
const nsfw = useSelector((state) => state.application.nsfw);
const isConnected = useSelector((state) => state.application.isConnected);
const currentAccount = useSelector((state) => state.account.currentAccount);
const initPosts = useSelector((state) => state.posts.initPosts)
const initPosts = useSelector((state) => state.posts.initPosts);
const username = currentAccount.username;
const userPinned = currentAccount.about?.profile?.pinned;
//state
const [posts, setPosts] = useState([]);
const [promotedPosts, setPromotedPosts] = useState([]);
const [sessionUser, setSessionUser] = useState(username);
const [tabMeta, setTabMeta] = useState(DEFAULT_TAB_META);
const [latestPosts, setLatestPosts] = useState<any[]>([]);
const [postFetchTimer, setPostFetchTimer] = useState(0)
const [postFetchTimer, setPostFetchTimer] = useState(0);
const [enableScrollTop, setEnableScrollTop] = useState(false);
const [curPinned, setCurPinned] = useState(pinnedPermlink)
const [curPinned, setCurPinned] = useState(pinnedPermlink);
//refs
let postsListRef = useRef<PostsListRef>()
let postsListRef = useRef<PostsListRef>();
const appState = useRef(AppState.currentState);
const postsRef = useRef(posts);
const sessionUserRef = useRef(sessionUser);
@ -72,12 +70,8 @@ const TabContent = ({
postsRef.current = posts;
sessionUserRef.current = sessionUser;
//side effects
useEffect(() => {
if (isFeedScreen) {
AppState.addEventListener('change', _handleAppStateChange);
}
@ -85,295 +79,287 @@ const TabContent = ({
_initContent(true, feedUsername);
return _cleanup;
}, [tag])
useEffect(()=>{
if(isConnected && (username !== sessionUser || forceLoadPosts)){
_initContent(false, username);
}
}, [username, forceLoadPosts])
}, [tag]);
useEffect(() => {
if(filterScrollRequest && filterScrollRequest === filterKey){
if (isConnected && (username !== sessionUser || forceLoadPosts)) {
_initContent(false, username);
}
}, [username, forceLoadPosts]);
useEffect(() => {
if (filterScrollRequest && filterScrollRequest === filterKey) {
_scrollToTop();
if(onScrollRequestProcessed){
if (onScrollRequestProcessed) {
onScrollRequestProcessed();
}
}
}, [filterScrollRequest])
}, [filterScrollRequest]);
useEffect(()=>{
console.log("curPinned change", userPinned);
if(pageType === 'ownProfile' && userPinned !== curPinned ){
useEffect(() => {
console.log('curPinned change', userPinned);
if (pageType === 'ownProfile' && userPinned !== curPinned) {
_scrollToTop();
_loadPosts({shouldReset:true, _pinnedPermlink:userPinned})
_loadPosts({ shouldReset: true, _pinnedPermlink: userPinned });
setCurPinned(userPinned);
}
},[userPinned])
}, [userPinned]);
const _cleanup = () => {
_isMounted = false;
if(postFetchTimer){
if (postFetchTimer) {
clearTimeout(postFetchTimer);
}
if (isFeedScreen) {
AppState.removeEventListener('change', _handleAppStateChange);
}
}
};
//actions
const _handleAppStateChange = (nextAppState) => {
if (appState.current.match(/inactive|background/) && nextAppState === 'active' && posts.length > 0) {
if (
appState.current.match(/inactive|background/) &&
nextAppState === 'active' &&
posts.length > 0
) {
const isLatestPostsCheck = true;
_loadPosts({
shouldReset:false,
isLatestPostsCheck
shouldReset: false,
isLatestPostsCheck,
});
}
appState.current = nextAppState;
};
const _initContent = (isFirstCall = false, _feedUsername:string) => {
const _initContent = (isFirstCall = false, _feedUsername: string) => {
_scrollToTop();
const initialPosts = isFirstCall && isFeedScreen && isInitialTab ? initPosts : [];
setPosts(initialPosts);
setTabMeta(DEFAULT_TAB_META)
setTabMeta(DEFAULT_TAB_META);
setSessionUser(_feedUsername);
setLatestPosts([]);
if(postFetchTimer){
if (postFetchTimer) {
clearTimeout(postFetchTimer);
}
if(username || (filterKey !== 'friends' && filterKey !== 'communities')){
if (username || (filterKey !== 'friends' && filterKey !== 'communities')) {
_loadPosts({
shouldReset:!isFirstCall,
shouldReset: !isFirstCall,
isFirstCall,
isLatestPostsCheck:false,
_feedUsername,
_posts:initialPosts,
_tabMeta:DEFAULT_TAB_META
isLatestPostsCheck: false,
_feedUsername,
_posts: initialPosts,
_tabMeta: DEFAULT_TAB_META,
});
_getPromotedPosts();
}
}
};
//fetch posts from server
const _loadPosts = async ({
shouldReset = false,
isLatestPostsCheck = false,
isFirstCall = false,
_feedUsername = isFeedScreen? sessionUserRef.current:feedUsername,
_feedUsername = isFeedScreen ? sessionUserRef.current : feedUsername,
_posts = postsRef.current,
_tabMeta = tabMeta,
_pinnedPermlink = curPinned
}:{
shouldReset?:boolean;
isLatestPostsCheck?:boolean;
isFirstCall?:boolean;
_feedUsername?:string;
_posts?:any[];
_tabMeta?:TabMeta;
_pinnedPermlink?:string
_pinnedPermlink = curPinned,
}: {
shouldReset?: boolean;
isLatestPostsCheck?: boolean;
isFirstCall?: boolean;
_feedUsername?: string;
_posts?: any[];
_tabMeta?: TabMeta;
_pinnedPermlink?: string;
}) => {
const options = {
setTabMeta:(meta:TabMeta) => {
if(_isMounted){
setTabMeta(meta)
setTabMeta: (meta: TabMeta) => {
if (_isMounted) {
setTabMeta(meta);
}
},
filterKey,
prevPosts:_posts,
tabMeta:_tabMeta,
prevPosts: _posts,
tabMeta: _tabMeta,
isLoggedIn,
nsfw,
isConnected,
isFeedScreen,
refreshing:shouldReset,
refreshing: shouldReset,
pageType,
isLatestPostsCheck,
feedUsername:_feedUsername,
pinnedPermlink:_pinnedPermlink,
feedUsername: _feedUsername,
pinnedPermlink: _pinnedPermlink,
tag,
...props
} as LoadPostsOptions
...props,
} as LoadPostsOptions;
const result = await loadPosts(options)
if(_isMounted && result){
if(shouldReset || isFirstCall){
const result = await loadPosts(options);
if (_isMounted && result) {
if (shouldReset || isFirstCall) {
setPosts([]);
}
_postProcessLoadResult(result)
_postProcessLoadResult(result);
}
}
};
const _getPromotedPosts = async () => {
if(pageType === 'profile' || pageType === 'ownProfile' || pageType === 'community'){
if (pageType === 'profile' || pageType === 'ownProfile' || pageType === 'community') {
return;
}
const pPosts = await fetchPromotedEntries(username)
if(pPosts){
setPromotedPosts(pPosts)
const pPosts = await fetchPromotedEntries(username);
if (pPosts) {
setPromotedPosts(pPosts);
}
}
};
//schedules post fetch
const _scheduleLatestPostsCheck = (firstPost:any) => {
const _scheduleLatestPostsCheck = (firstPost: any) => {
if (firstPost) {
if (postFetchTimer) {
clearTimeout(postFetchTimer);
}
const timeLeft = calculateTimeLeftForPostCheck(firstPost)
const timeLeft = calculateTimeLeftForPostCheck(firstPost);
const _postFetchTimer = setTimeout(() => {
const isLatestPostsCheck = true;
_loadPosts({
shouldReset:false,
isLatestPostsCheck
});
},
timeLeft
);
setPostFetchTimer(_postFetchTimer)
const isLatestPostsCheck = true;
_loadPosts({
shouldReset: false,
isLatestPostsCheck,
});
}, timeLeft);
setPostFetchTimer(_postFetchTimer);
}
};
//processes response from loadPost
const _postProcessLoadResult = ({updatedPosts, latestPosts}:any) => {
const _postProcessLoadResult = ({ updatedPosts, latestPosts }: any) => {
//process new posts avatart
if(latestPosts && Array.isArray(latestPosts)){
if(latestPosts.length > 0){
setLatestPosts(latestPosts)
}else{
_scheduleLatestPostsCheck(posts[0])
if (latestPosts && Array.isArray(latestPosts)) {
if (latestPosts.length > 0) {
setLatestPosts(latestPosts);
} else {
_scheduleLatestPostsCheck(posts[0]);
}
}
//process returned data
if(Array.isArray(updatedPosts)){
if(updatedPosts.length){
if (Array.isArray(updatedPosts)) {
if (updatedPosts.length) {
//match new and old first post
const firstPostChanged = posts.length == 0 || (posts[0].permlink !== updatedPosts[0].permlink);
const firstPostChanged =
posts.length == 0 || posts[0].permlink !== updatedPosts[0].permlink;
if (isFeedScreen && firstPostChanged) {
//schedule refetch of new posts by checking time of current post
_scheduleLatestPostsCheck(updatedPosts[0]);
//schedule refetch of new posts by checking time of current post
_scheduleLatestPostsCheck(updatedPosts[0]);
if (isInitialTab) {
dispatch(setInitPosts(updatedPosts));
}
if (isInitialTab) {
dispatch(setInitPosts(updatedPosts));
}
}
} else if (isFeedScreen && isInitialTab){
} else if (isFeedScreen && isInitialTab) {
//clear posts cache if no first tab posts available, precautionary measure for accoutn change
dispatch(setInitPosts([]))
dispatch(setInitPosts([]));
}
setPosts(updatedPosts);
}
}
};
//view related routines
const _onPostsPopupPress = () => {
_scrollToTop();
_getPromotedPosts()
setPosts([...latestPosts, ...posts])
_scheduleLatestPostsCheck(latestPosts[0]);
setLatestPosts([]);
}
_scrollToTop();
_getPromotedPosts();
setPosts([...latestPosts, ...posts]);
_scheduleLatestPostsCheck(latestPosts[0]);
setLatestPosts([]);
};
const _scrollToTop = () => {
postsListRef.current.scrollToTop();
setEnableScrollTop(false);
scrollPopupDebouce.cancel();
blockPopup = true;
setTimeout(()=>{
setTimeout(() => {
blockPopup = false;
}, 1000)
}, 1000);
};
const _handleOnScroll = () => {
if(handleOnScroll){
handleOnScroll()
if (handleOnScroll) {
handleOnScroll();
}
}
};
//view rendereres
const _renderEmptyContent = () => {
return <TabEmptyView filterKey={filterKey} isNoPost={tabMeta.isNoPost}/>
}
return <TabEmptyView filterKey={filterKey} isNoPost={tabMeta.isNoPost} />;
};
const scrollPopupDebouce = debounce(
(value) => {
setEnableScrollTop(value);
},
500,
{ leading: true },
);
const scrollPopupDebouce = debounce((value)=>{
setEnableScrollTop(value);
}, 500, {leading:true})
const _onScroll = (event:NativeSyntheticEvent<NativeScrollEvent>)=>{
const _onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
var currentOffset = event.nativeEvent.contentOffset.y;
var scrollUp = currentOffset < scrollOffset;
scrollOffset = currentOffset;
if(scrollUp && !blockPopup && currentOffset > SCROLL_POPUP_THRESHOLD){
scrollPopupDebouce(true)
if (scrollUp && !blockPopup && currentOffset > SCROLL_POPUP_THRESHOLD) {
scrollPopupDebouce(true);
}
};
// show quick reply modal
const _showQuickReplyModal = (post:any) => {
const _showQuickReplyModal = (post: any) => {
if (isLoggedIn) {
dispatch(showReplyModal(post))
dispatch(showReplyModal(post));
} else {
//TODO: show proper alert message
//TODO: show proper alert message
console.log('Not LoggedIn');
}
}
};
return (
<>
<PostsList
ref={postsListRef}
data={posts}
isFeedScreen={isFeedScreen}
promotedPosts={promotedPosts}
onLoadPosts={(shouldReset)=>{
_loadPosts({shouldReset})
if(shouldReset){
_getPromotedPosts()
}
}}
onScroll={_onScroll}
onScrollEndDrag={_handleOnScroll}
isRefreshing={tabMeta.isRefreshing}
isLoading={tabMeta.isLoading}
ListEmptyComponent={_renderEmptyContent}
pageType={pageType}
showQuickReplyModal={_showQuickReplyModal}
/>
<ScrollTopPopup
popupAvatars={latestPosts.map(post=>post.avatar || '')}
enableScrollTop={enableScrollTop}
onPress={_onPostsPopupPress}
onClose={()=>{
setLatestPosts([])
setEnableScrollTop(false);
}}
/>
</>
<PostsList
ref={postsListRef}
data={posts}
isFeedScreen={isFeedScreen}
promotedPosts={promotedPosts}
onLoadPosts={(shouldReset) => {
_loadPosts({ shouldReset });
if (shouldReset) {
_getPromotedPosts();
}
}}
onScroll={_onScroll}
onScrollEndDrag={_handleOnScroll}
isRefreshing={tabMeta.isRefreshing}
isLoading={tabMeta.isLoading}
ListEmptyComponent={_renderEmptyContent}
pageType={pageType}
showQuickReplyModal={_showQuickReplyModal}
/>
<ScrollTopPopup
popupAvatars={latestPosts.map((post) => post.avatar || '')}
enableScrollTop={enableScrollTop}
onPress={_onPostsPopupPress}
onClose={() => {
setLatestPosts([]);
setEnableScrollTop(false);
}}
/>
</>
);
};
export default TabContent;

View File

@ -7,13 +7,13 @@ import { useAppSelector } from '../../../hooks';
import styles from './textInputStyles';
interface Props extends TextInputProps {
innerRef: Ref<TextInput>,
height: number,
style: TextStyle
innerRef: Ref<TextInput>;
height: number;
style: TextStyle;
}
const TextInputView = ({ innerRef, height, style, ...props }: Props) => {
const isDarkTheme = useAppSelector(state => state.application.isDarkTheme);
const isDarkTheme = useAppSelector((state) => state.application.isDarkTheme);
return (
<TextInput
ref={innerRef}
@ -22,7 +22,7 @@ const TextInputView = ({ innerRef, height, style, ...props }: Props) => {
{...props}
style={[styles.input, { minHeight: height }, style]}
/>
)
);
};
export default TextInputView;

View File

@ -54,7 +54,7 @@ export default EStyleSheet.create({
textAlign: 'right',
},
centerDescription: {
marginTop:8,
marginTop: 8,
fontSize: 12,
color: '$primaryBlack',
fontWeight: '600',

View File

@ -2,13 +2,13 @@ import { proxifyImageSrc } from '@ecency/render-helper';
import React, { useEffect, useRef, useState } from 'react';
import { useIntl } from 'react-intl';
import {
ActivityIndicator,
Alert,
Keyboard,
Platform,
Text,
TouchableOpacity,
View,
ActivityIndicator,
Alert,
Keyboard,
Platform,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { View as AnimatedView } from 'react-native-animatable';
import Animated, { Easing } from 'react-native-reanimated';
@ -18,312 +18,311 @@ import { FlatList } from 'react-native-gesture-handler';
import { Icon, IconButton } from '../..';
import { UploadedMedia } from '../../../models';
import styles, {
COMPACT_HEIGHT,
EXPANDED_HEIGHT,
MAX_HORIZONTAL_THUMBS,
COMPACT_HEIGHT,
EXPANDED_HEIGHT,
MAX_HORIZONTAL_THUMBS,
} from './uploadsGalleryModalStyles';
import { useMediaDeleteMutation } from '../../../providers/queries';
type Props = {
insertedMediaUrls: string[];
mediaUploads: any[];
indices: Map<number, boolean>;
isAddingToUploads: boolean;
getMediaUploads: () => void;
deleteMedia: (ids: string) => Promise<boolean>;
insertMedia: (map: Map<number, boolean>) => void;
handleOpenGallery: (addToUploads?: boolean) => void;
handleOpenCamera: () => void;
handleOpenForUpload: () => void;
insertedMediaUrls: string[];
mediaUploads: any[];
indices: Map<number, boolean>;
isAddingToUploads: boolean;
getMediaUploads: () => void;
deleteMedia: (ids: string) => Promise<boolean>;
insertMedia: (map: Map<number, boolean>) => void;
handleOpenGallery: (addToUploads?: boolean) => void;
handleOpenCamera: () => void;
handleOpenForUpload: () => void;
};
const UploadsGalleryContent = ({
insertedMediaUrls,
mediaUploads,
isAddingToUploads,
getMediaUploads,
deleteMedia,
insertMedia,
handleOpenGallery,
handleOpenCamera,
insertedMediaUrls,
mediaUploads,
isAddingToUploads,
getMediaUploads,
deleteMedia,
insertMedia,
handleOpenGallery,
handleOpenCamera,
}: Props) => {
const intl = useIntl();
const intl = useIntl();
const deleteMediaMutation = useMediaDeleteMutation();
const deleteMediaMutation = useMediaDeleteMutation();
const [deleteIds, setDeleteIds] = useState<string[]>([]);
const [isDeleteMode, setIsDeleteMode] = useState(false);
const [isExpandedMode, setIsExpandedMode] = useState(false);
const [deleteIds, setDeleteIds] = useState<string[]>([]);
const [isDeleteMode, setIsDeleteMode] = useState(false);
const [isExpandedMode, setIsExpandedMode] = useState(false);
const animatedHeightRef = useRef(new Animated.Value(COMPACT_HEIGHT));
const animatedHeightRef = useRef(new Animated.Value(COMPACT_HEIGHT));
const isDeleting = deleteMediaMutation.isLoading
const isDeleting = deleteMediaMutation.isLoading;
useEffect(() => {
if (isExpandedMode) {
Keyboard.dismiss();
}
}, [isExpandedMode]);
useEffect(() => {
if (isExpandedMode) {
Keyboard.dismiss();
}
}, [isExpandedMode]);
const _deleteMedia = async () => {
deleteMediaMutation.mutate(deleteIds, {
onSettled: () => {
setIsDeleteMode(false);
setDeleteIds([]);
}
});
};
const _deleteMedia = async () => {
deleteMediaMutation.mutate(deleteIds, {
onSettled: () => {
setIsDeleteMode(false);
setDeleteIds([]);
},
});
};
const _onDeletePress = async () => {
if (isDeleteMode && deleteIds.length > 0) {
const _onCancelPress = () => {
setIsDeleteMode(false);
setDeleteIds([]);
};
const _onDeletePress = async () => {
if (isDeleteMode && deleteIds.length > 0) {
const _onCancelPress = () => {
setIsDeleteMode(false);
setDeleteIds([]);
};
Alert.alert(
intl.formatMessage({ id: 'alert.delete' }),
intl.formatMessage({ id: 'uploads_modal.confirm_delete' }),
[
{
text: intl.formatMessage({ id: 'alert.cancel' }),
style: 'cancel',
onPress: _onCancelPress,
},
{
text: intl.formatMessage({ id: 'alert.confirm' }),
onPress: () => _deleteMedia(),
},
],
);
Alert.alert(
intl.formatMessage({ id: 'alert.delete' }),
intl.formatMessage({ id: 'uploads_modal.confirm_delete' }),
[
{
text: intl.formatMessage({ id: 'alert.cancel' }),
style: 'cancel',
onPress: _onCancelPress,
},
{
text: intl.formatMessage({ id: 'alert.confirm' }),
onPress: () => _deleteMedia(),
},
],
);
} else {
setIsDeleteMode(!isDeleteMode);
}
};
//render list item for snippet and handle actions;
const _renderItem = ({ item, index }: { item: UploadedMedia; index: number }) => {
const _onPress = () => {
if (isDeleteMode) {
const idIndex = deleteIds.indexOf(item._id);
if (idIndex >= 0) {
deleteIds.splice(idIndex, 1);
} else {
setIsDeleteMode(!isDeleteMode);
deleteIds.push(item._id);
}
setDeleteIds([...deleteIds]);
} else {
insertMedia(new Map([[index, true]]));
}
};
//render list item for snippet and handle actions;
const _renderItem = ({ item, index }: { item: UploadedMedia; index: number }) => {
const _onPress = () => {
if (isDeleteMode) {
const idIndex = deleteIds.indexOf(item._id);
if (idIndex >= 0) {
deleteIds.splice(idIndex, 1);
} else {
deleteIds.push(item._id);
}
setDeleteIds([...deleteIds]);
} else {
insertMedia(new Map([[index, true]]));
}
};
const thumbUrl = proxifyImageSrc(item.url, 600, 500, Platform.OS === 'ios' ? 'match' : 'webp');
let isInsertedTimes = 0;
insertedMediaUrls.forEach((url) => (isInsertedTimes += url === item.url ? 1 : 0));
const isToBeDeleted = deleteIds.indexOf(item._id) >= 0;
const transformStyle = {
transform: isToBeDeleted ? [{ scaleX: 0.7 }, { scaleY: 0.7 }] : [],
};
const _renderMinus = () =>
isDeleteMode && (
<AnimatedView animation="zoomIn" duration={300} style={styles.minusContainer}>
<Icon
color={EStyleSheet.value('$pureWhite')}
iconType="MaterialCommunityIcons"
name="minus"
size={20}
/>
</AnimatedView>
);
const _renderCounter = () =>
isInsertedTimes > 0 &&
!isDeleteMode && (
<AnimatedView animation="zoomIn" duration={300} style={styles.counterContainer}>
<Text style={styles.counterText}>{isInsertedTimes}</Text>
</AnimatedView>
);
return (
<TouchableOpacity onPress={_onPress} disabled={isDeleting}>
<View style={transformStyle}>
<FastImage
source={{ uri: thumbUrl }}
style={isExpandedMode ? styles.gridMediaItem : styles.mediaItem}
/>
{_renderCounter()}
{_renderMinus()}
</View>
</TouchableOpacity>
);
const thumbUrl = proxifyImageSrc(item.url, 600, 500, Platform.OS === 'ios' ? 'match' : 'webp');
let isInsertedTimes = 0;
insertedMediaUrls.forEach((url) => (isInsertedTimes += url === item.url ? 1 : 0));
const isToBeDeleted = deleteIds.indexOf(item._id) >= 0;
const transformStyle = {
transform: isToBeDeleted ? [{ scaleX: 0.7 }, { scaleY: 0.7 }] : [],
};
const _renderSelectButton = (iconName: string, text: string, onPress: () => void) => {
return (
<TouchableOpacity
onPress={() => {
onPress && onPress();
}}
>
<View style={styles.selectButton}>
<View style={styles.selectBtnPlus}>
<Icon
color={EStyleSheet.value('$primaryBackgroundColor')}
iconType="FontAwesome5"
name="plus-circle"
size={12}
/>
</View>
<Icon
color={EStyleSheet.value('$primaryBlack')}
iconType="MaterialCommunityIcons"
name={iconName}
size={24}
/>
<Text style={styles.selectButtonLabel}>{text}</Text>
</View>
</TouchableOpacity>
);
};
const _renderHeaderContent = () => (
<View style={{ ...styles.buttonsContainer, paddingVertical: isExpandedMode ? 8 : 0 }}>
<View style={styles.selectButtonsContainer}>
{_renderSelectButton('image', 'Gallery', handleOpenGallery)}
{_renderSelectButton('camera', 'Camera', handleOpenCamera)}
</View>
<View style={styles.pillBtnContainer}>
<IconButton
style={styles.uploadsActionBtn}
color={EStyleSheet.value('$primaryBlack')}
iconType="MaterialCommunityIcons"
name="plus"
size={28}
onPress={() => {
handleOpenGallery(true);
}}
/>
<IconButton
style={{
...styles.uploadsActionBtn,
backgroundColor: isDeleteMode ? EStyleSheet.value('$iconColor') : 'transparent',
}}
color={EStyleSheet.value('$primaryBlack')}
iconType="MaterialCommunityIcons"
name="minus"
size={28}
onPress={() => {
setIsDeleteMode(!isDeleteMode);
setDeleteIds([]);
}}
/>
</View>
{isAddingToUploads && (
<View style={styles.pillBtnContainer}>
<ActivityIndicator color={EStyleSheet.value('$primaryBlack')} />
</View>
)}
{isExpandedMode && _renderExpansionButton()}
{isExpandedMode && _renderDeleteButton()}
</View>
);
//render empty list placeholder
const _renderEmptyContent = () => {
return (
<>
<Text style={styles.emptyText}>
{intl.formatMessage({ id: 'uploads_modal.label_no_images' })}
</Text>
</>
);
};
const _renderExpansionButton = () => (
<IconButton
style={styles.pillBtnContainer}
const _renderMinus = () =>
isDeleteMode && (
<AnimatedView animation="zoomIn" duration={300} style={styles.minusContainer}>
<Icon
color={EStyleSheet.value('$pureWhite')}
iconType="MaterialCommunityIcons"
name={isExpandedMode ? 'arrow-collapse-vertical' : 'arrow-expand-vertical'}
color={EStyleSheet.value('$primaryBlack')}
size={32}
onPress={() => {
Animated.timing(animatedHeightRef.current, {
toValue: isExpandedMode ? COMPACT_HEIGHT : EXPANDED_HEIGHT,
duration: 300,
easing: Easing.inOut(Easing.cubic),
}).start(() => {
setIsExpandedMode(!isExpandedMode);
});
}}
/>
);
name="minus"
size={20}
/>
</AnimatedView>
);
const _renderDeleteButton = () => {
if (deleteIds.length > 0) {
return isExpandedMode ? (
<IconButton
style={{
...styles.pillBtnContainer,
backgroundColor: EStyleSheet.value('$primaryRed'),
}}
iconType="MaterialCommunityIcons"
name="delete-outline"
color={EStyleSheet.value(deleteIds.length > 0 ? '$primaryBlack' : '$primaryBlack')}
size={32}
onPress={_onDeletePress}
isLoading={isDeleting}
/>
) : (
<AnimatedView
animation={deleteIds.length > 0 ? 'slideInRight' : 'slideOutRight'}
duration={300}
style={styles.deleteButtonContainer}
>
<IconButton
style={styles.deleteButton}
color={EStyleSheet.value('$primaryBlack')}
iconType="MaterialCommunityIcons"
name="delete-outline"
disabled={isDeleting}
size={28}
onPress={_onDeletePress}
isLoading={isDeleting}
/>
</AnimatedView>
);
}
return null;
};
const _renderCounter = () =>
isInsertedTimes > 0 &&
!isDeleteMode && (
<AnimatedView animation="zoomIn" duration={300} style={styles.counterContainer}>
<Text style={styles.counterText}>{isInsertedTimes}</Text>
</AnimatedView>
);
return (
<Animated.View style={{ ...styles.container, height: animatedHeightRef.current }}>
<FlatList
key={isExpandedMode ? 'vertical_grid' : 'horizontal_list'}
data={mediaUploads.slice(0, !isExpandedMode ? MAX_HORIZONTAL_THUMBS : undefined)}
keyExtractor={(item) => `item_${item.url}`}
renderItem={_renderItem}
style={{ flex: 1 }}
contentContainerStyle={
isExpandedMode ? styles.gridContentContainer : styles.listContentContainer
}
ListHeaderComponent={_renderHeaderContent}
ListEmptyComponent={_renderEmptyContent}
ListFooterComponent={!isExpandedMode && mediaUploads.length > 0 && _renderExpansionButton}
extraData={deleteIds}
horizontal={!isExpandedMode}
numColumns={isExpandedMode ? 2 : 1}
keyboardShouldPersistTaps="always"
/>
{!isExpandedMode && _renderDeleteButton()}
</Animated.View>
<TouchableOpacity onPress={_onPress} disabled={isDeleting}>
<View style={transformStyle}>
<FastImage
source={{ uri: thumbUrl }}
style={isExpandedMode ? styles.gridMediaItem : styles.mediaItem}
/>
{_renderCounter()}
{_renderMinus()}
</View>
</TouchableOpacity>
);
};
const _renderSelectButton = (iconName: string, text: string, onPress: () => void) => {
return (
<TouchableOpacity
onPress={() => {
onPress && onPress();
}}
>
<View style={styles.selectButton}>
<View style={styles.selectBtnPlus}>
<Icon
color={EStyleSheet.value('$primaryBackgroundColor')}
iconType="FontAwesome5"
name="plus-circle"
size={12}
/>
</View>
<Icon
color={EStyleSheet.value('$primaryBlack')}
iconType="MaterialCommunityIcons"
name={iconName}
size={24}
/>
<Text style={styles.selectButtonLabel}>{text}</Text>
</View>
</TouchableOpacity>
);
};
const _renderHeaderContent = () => (
<View style={{ ...styles.buttonsContainer, paddingVertical: isExpandedMode ? 8 : 0 }}>
<View style={styles.selectButtonsContainer}>
{_renderSelectButton('image', 'Gallery', handleOpenGallery)}
{_renderSelectButton('camera', 'Camera', handleOpenCamera)}
</View>
<View style={styles.pillBtnContainer}>
<IconButton
style={styles.uploadsActionBtn}
color={EStyleSheet.value('$primaryBlack')}
iconType="MaterialCommunityIcons"
name="plus"
size={28}
onPress={() => {
handleOpenGallery(true);
}}
/>
<IconButton
style={{
...styles.uploadsActionBtn,
backgroundColor: isDeleteMode ? EStyleSheet.value('$iconColor') : 'transparent',
}}
color={EStyleSheet.value('$primaryBlack')}
iconType="MaterialCommunityIcons"
name="minus"
size={28}
onPress={() => {
setIsDeleteMode(!isDeleteMode);
setDeleteIds([]);
}}
/>
</View>
{isAddingToUploads && (
<View style={styles.pillBtnContainer}>
<ActivityIndicator color={EStyleSheet.value('$primaryBlack')} />
</View>
)}
{isExpandedMode && _renderExpansionButton()}
{isExpandedMode && _renderDeleteButton()}
</View>
);
//render empty list placeholder
const _renderEmptyContent = () => {
return (
<>
<Text style={styles.emptyText}>
{intl.formatMessage({ id: 'uploads_modal.label_no_images' })}
</Text>
</>
);
};
const _renderExpansionButton = () => (
<IconButton
style={styles.pillBtnContainer}
iconType="MaterialCommunityIcons"
name={isExpandedMode ? 'arrow-collapse-vertical' : 'arrow-expand-vertical'}
color={EStyleSheet.value('$primaryBlack')}
size={32}
onPress={() => {
Animated.timing(animatedHeightRef.current, {
toValue: isExpandedMode ? COMPACT_HEIGHT : EXPANDED_HEIGHT,
duration: 300,
easing: Easing.inOut(Easing.cubic),
}).start(() => {
setIsExpandedMode(!isExpandedMode);
});
}}
/>
);
const _renderDeleteButton = () => {
if (deleteIds.length > 0) {
return isExpandedMode ? (
<IconButton
style={{
...styles.pillBtnContainer,
backgroundColor: EStyleSheet.value('$primaryRed'),
}}
iconType="MaterialCommunityIcons"
name="delete-outline"
color={EStyleSheet.value(deleteIds.length > 0 ? '$primaryBlack' : '$primaryBlack')}
size={32}
onPress={_onDeletePress}
isLoading={isDeleting}
/>
) : (
<AnimatedView
animation={deleteIds.length > 0 ? 'slideInRight' : 'slideOutRight'}
duration={300}
style={styles.deleteButtonContainer}
>
<IconButton
style={styles.deleteButton}
color={EStyleSheet.value('$primaryBlack')}
iconType="MaterialCommunityIcons"
name="delete-outline"
disabled={isDeleting}
size={28}
onPress={_onDeletePress}
isLoading={isDeleting}
/>
</AnimatedView>
);
}
return null;
};
return (
<Animated.View style={{ ...styles.container, height: animatedHeightRef.current }}>
<FlatList
key={isExpandedMode ? 'vertical_grid' : 'horizontal_list'}
data={mediaUploads.slice(0, !isExpandedMode ? MAX_HORIZONTAL_THUMBS : undefined)}
keyExtractor={(item) => `item_${item.url}`}
renderItem={_renderItem}
style={{ flex: 1 }}
contentContainerStyle={
isExpandedMode ? styles.gridContentContainer : styles.listContentContainer
}
ListHeaderComponent={_renderHeaderContent}
ListEmptyComponent={_renderEmptyContent}
ListFooterComponent={!isExpandedMode && mediaUploads.length > 0 && _renderExpansionButton}
extraData={deleteIds}
horizontal={!isExpandedMode}
numColumns={isExpandedMode ? 2 : 1}
keyboardShouldPersistTaps="always"
/>
{!isExpandedMode && _renderDeleteButton()}
</Animated.View>
);
};
export default UploadsGalleryContent;

View File

@ -15,7 +15,7 @@ export default EStyleSheet.create({
},
listContentContainer: {
marginTop:8,
marginTop: 8,
paddingRight: 72,
paddingLeft: 16,
},
@ -30,7 +30,7 @@ export default EStyleSheet.create({
height: THUMB_SIZE,
width: THUMB_SIZE,
borderRadius: 16,
backgroundColor: '$primaryLightGray'
backgroundColor: '$primaryLightGray',
} as ImageStyle,
gridMediaItem: {
@ -39,11 +39,11 @@ export default EStyleSheet.create({
width: GRID_THUMB_SIZE,
marginVertical: 8,
borderRadius: 16,
backgroundColor: '$primaryLightGray'
backgroundColor: '$primaryLightGray',
} as ImageStyle,
inputContainer: {
flex: 1
flex: 1,
} as ViewStyle,
titleInput: {
color: '$primaryBlack',
@ -53,7 +53,7 @@ export default EStyleSheet.create({
paddingVertical: 0,
backgroundColor: '$primaryBackgroundColor',
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '$primaryDarkGray'
borderBottomColor: '$primaryDarkGray',
} as TextStyle,
title: {
@ -69,20 +69,19 @@ export default EStyleSheet.create({
fontSize: 16,
color: '$primaryBlack',
marginLeft: 12,
alignSelf:'center'
alignSelf: 'center',
},
btnText: {
color: '$pureWhite'
color: '$pureWhite',
} as TextStyle,
saveButton: {
backgroundColor: '$primaryBlue',
width: 150,
paddingVertical: 16,
borderRadius: 32,
justifyContent: 'center',
alignItems: 'center'
alignItems: 'center',
} as ViewStyle,
closeButton: {
@ -90,14 +89,14 @@ export default EStyleSheet.create({
paddingVertical: 8,
borderRadius: 16,
justifyContent: 'center',
alignItems: 'center'
alignItems: 'center',
} as ViewStyle,
actionPanel: {
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
marginBottom: 16
marginBottom: 16,
} as ViewStyle,
itemIcon: {
@ -113,8 +112,8 @@ export default EStyleSheet.create({
selectButtonsContainer: {
justifyContent: 'space-around',
paddingVertical: 8,
marginRight:8,
height: THUMB_SIZE
marginRight: 8,
height: THUMB_SIZE,
} as ViewStyle,
selectButton: {
@ -128,14 +127,14 @@ export default EStyleSheet.create({
marginTop: -16,
zIndex: 2,
borderRadius: 12,
backgroundColor: "$primaryBlack"
backgroundColor: '$primaryBlack',
} as ViewStyle,
selectButtonLabel: {
fontSize: 16,
textAlignVertical: 'top',
color: '$primaryBlack',
marginLeft: 4
marginLeft: 4,
} as TextStyle,
pillBtnContainer: {
@ -153,16 +152,15 @@ export default EStyleSheet.create({
width: THUMB_SIZE / 1.8,
borderRadius: 0,
borderBottomLeftRadius: 20,
borderBottomRightRadius: 20
borderBottomRightRadius: 20,
} as ViewStyle,
deleteButtonContainer: {
position: 'absolute',
right: 0,
top: 0,
bottom: 0,
justifyContent: 'center'
justifyContent: 'center',
},
deleteButton: {
@ -171,14 +169,13 @@ export default EStyleSheet.create({
borderRadius: 0,
borderBottomLeftRadius: 20,
borderTopLeftRadius: 20,
backgroundColor: '$primaryRed'
backgroundColor: '$primaryRed',
} as ViewStyle,
itemIconWrapper: {
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '$primaryRed',
} as ViewStyle,
minusContainer: {
@ -200,15 +197,14 @@ export default EStyleSheet.create({
height: 24,
width: 24,
justifyContent: 'center',
alignItems: 'center'
alignItems: 'center',
} as ViewStyle,
counterText: {
color: '$primaryBlack',
fontSize: 16
fontSize: 16,
} as TextStyle,
checkStyle: {
backgroundColor: '$white',
} as ViewStyle,
@ -224,7 +220,6 @@ export default EStyleSheet.create({
marginLeft: 8,
borderRadius: 20,
justifyContent: 'center',
alignItems: 'center'
} as ViewStyle
})
alignItems: 'center',
} as ViewStyle,
});

View File

@ -12,339 +12,341 @@ import showLoginAlert from '../../../utils/showLoginAlert';
import { useMediaQuery, useMediaUploadMutation } from '../../../providers/queries';
export interface UploadsGalleryModalRef {
showModal: () => void;
showModal: () => void;
}
export enum MediaInsertStatus {
UPLOADING = 'UPLOADING',
READY = 'READY',
FAILED = 'FAILED',
UPLOADING = 'UPLOADING',
READY = 'READY',
FAILED = 'FAILED',
}
export interface MediaInsertData {
url: string;
filename?: string;
text: string;
status: MediaInsertStatus;
url: string;
filename?: string;
text: string;
status: MediaInsertStatus;
}
interface UploadsGalleryModalProps {
insertedMediaUrls: string[];
paramFiles: any[];
username: string;
isEditing: boolean;
isPreviewActive: boolean;
hideToolbarExtension: () => void;
handleMediaInsert: (data: Array<MediaInsertData>) => void;
setIsUploading: (status: boolean) => void;
insertedMediaUrls: string[];
paramFiles: any[];
username: string;
isEditing: boolean;
isPreviewActive: boolean;
hideToolbarExtension: () => void;
handleMediaInsert: (data: Array<MediaInsertData>) => void;
setIsUploading: (status: boolean) => void;
}
export const UploadsGalleryModal = forwardRef(
(
{
insertedMediaUrls,
paramFiles,
username,
isEditing,
isPreviewActive,
hideToolbarExtension,
handleMediaInsert,
setIsUploading,
}: UploadsGalleryModalProps,
ref,
) => {
const intl = useIntl();
const dispatch = useAppDispatch();
(
{
insertedMediaUrls,
paramFiles,
username,
isEditing,
isPreviewActive,
hideToolbarExtension,
handleMediaInsert,
setIsUploading,
}: UploadsGalleryModalProps,
ref,
) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const mediaQuery = useMediaQuery();
const mediaUploadMutation = useMediaUploadMutation();
const mediaQuery = useMediaQuery();
const mediaUploadMutation = useMediaUploadMutation();
const pendingInserts = useRef<MediaInsertData[]>([]);
const pendingInserts = useRef<MediaInsertData[]>([]);
const [mediaUploads, setMediaUploads] = useState([]);
const [showModal, setShowModal] = useState(false);
const [isAddingToUploads, setIsAddingToUploads] = useState(false);
const [mediaUploads, setMediaUploads] = useState([]);
const [showModal, setShowModal] = useState(false);
const [isAddingToUploads, setIsAddingToUploads] = useState(false);
const isLoggedIn = useAppSelector((state) => state.application.isLoggedIn);
const pinCode = useAppSelector((state) => state.application.pin);
const currentAccount = useAppSelector((state) => state.account.currentAccount);
const isLoggedIn = useAppSelector((state) => state.application.isLoggedIn);
const pinCode = useAppSelector((state) => state.application.pin);
const currentAccount = useAppSelector((state) => state.account.currentAccount);
useImperativeHandle(ref, () => ({
toggleModal: (value: boolean) => {
if (!isLoggedIn) {
showLoginAlert({ intl });
return;
}
useImperativeHandle(ref, () => ({
toggleModal: (value: boolean) => {
if (!isLoggedIn) {
showLoginAlert({ intl });
return;
}
if (value === showModal) {
return;
}
if (value === showModal) {
return;
}
if (value) {
_getMediaUploads();
}
setShowModal(value);
if (value) {
_getMediaUploads();
}
setShowModal(value);
},
}));
useEffect(() => {
if (paramFiles) {
console.log('files : ', paramFiles);
//delay is a workaround to let editor ready before initiating uploads on mount
delay(500).then(() => {
const _mediaItems = paramFiles.map((el) => {
if (el.filePath && el.fileName) {
const _media = {
path: el.filePath,
mime: el.mimeType,
filename: el.fileName,
};
return _media;
}
return null;
});
_handleMediaOnSelected(_mediaItems, true);
});
}
}, [paramFiles]);
useEffect(() => {
if (!isEditing && pendingInserts.current.length) {
handleMediaInsert(pendingInserts.current);
pendingInserts.current = [];
}
}, [isEditing]);
const _handleOpenImagePicker = (addToUploads?: boolean) => {
ImagePicker.openPicker({
includeBase64: true,
multiple: true,
mediaType: 'photo',
smartAlbums: ['UserLibrary', 'Favorites', 'PhotoStream', 'Panoramas', 'Bursts'],
})
.then((images) => {
if (images && !Array.isArray(images)) {
images = [images];
}
_handleMediaOnSelected(images, !addToUploads);
})
.catch((e) => {
_handleMediaOnSelectFailure(e);
});
};
const _handleOpenCamera = () => {
ImagePicker.openCamera({
includeBase64: true,
mediaType: 'photo',
})
.then((image) => {
_handleMediaOnSelected([image], true);
})
.catch((e) => {
_handleMediaOnSelectFailure(e);
});
};
const _handleMediaOnSelected = async (media: Image[], shouldInsert: boolean) => {
try {
if (!media || media.length == 0) {
throw new Error('New media items returned');
}
if (shouldInsert) {
setShowModal(false);
hideToolbarExtension();
media.forEach((element, index) => {
if (element) {
media[index].filename =
element.filename ||
extractFilenameFromPath({ path: element.path, mimeType: element.mime });
handleMediaInsert([
{
filename: element.filename,
url: '',
text: '',
status: MediaInsertStatus.UPLOADING,
},
]);
}
});
}
for (let index = 0; index < media.length; index++) {
const element = media[index];
if (element) {
await _uploadImage(element, { shouldInsert });
}
}
} catch (error) {
console.log('Failed to upload image', error);
bugsnapInstance.notify(error);
}
};
const _uploadImage = async (media, { shouldInsert } = { shouldInsert: false }) => {
if (!isLoggedIn) return;
try {
if (setIsUploading) {
setIsUploading(true);
}
if (!shouldInsert) {
setIsAddingToUploads(true);
}
await mediaUploadMutation.mutateAsync(
{
media,
addToUploads: !shouldInsert,
},
{
onSuccess: (data) => {
console.log('upload successfully', data, media, shouldInsert);
if (data && data.url && shouldInsert) {
_handleMediaInsertion({
filename: media.filename,
url: data.url,
text: '',
status: MediaInsertStatus.READY,
});
}
},
}));
useEffect(() => {
if (paramFiles) {
console.log('files : ', paramFiles);
//delay is a workaround to let editor ready before initiating uploads on mount
delay(500).then(() => {
const _mediaItems = paramFiles.map((el) => {
if (el.filePath && el.fileName) {
const _media = {
path: el.filePath,
mime: el.mimeType,
filename: el.fileName,
};
return _media;
}
return null;
});
_handleMediaOnSelected(_mediaItems, true);
});
}
}, [paramFiles]);
useEffect(() => {
if (!isEditing && pendingInserts.current.length) {
handleMediaInsert(pendingInserts.current);
pendingInserts.current = [];
}
}, [isEditing]);
const _handleOpenImagePicker = (addToUploads?: boolean) => {
ImagePicker.openPicker({
includeBase64: true,
multiple: true,
mediaType: 'photo',
smartAlbums: ['UserLibrary', 'Favorites', 'PhotoStream', 'Panoramas', 'Bursts'],
})
.then((images) => {
if (images && !Array.isArray(images)) {
images = [images];
}
_handleMediaOnSelected(images, !addToUploads);
})
.catch((e) => {
_handleMediaOnSelectFailure(e);
});
};
const _handleOpenCamera = () => {
ImagePicker.openCamera({
includeBase64: true,
mediaType: 'photo',
})
.then((image) => {
_handleMediaOnSelected([image], true);
})
.catch((e) => {
_handleMediaOnSelectFailure(e);
});
};
const _handleMediaOnSelected = async (media: Image[], shouldInsert: boolean) => {
try {
if (!media || media.length == 0) {
throw new Error('New media items returned');
}
if (shouldInsert) {
setShowModal(false);
hideToolbarExtension();
media.forEach((element, index) => {
if (element) {
media[index].filename =
element.filename ||
extractFilenameFromPath({ path: element.path, mimeType: element.mime });
handleMediaInsert([
{
filename: element.filename,
url: '',
text: '',
status: MediaInsertStatus.UPLOADING,
},
]);
}
});
}
for (let index = 0; index < media.length; index++) {
const element = media[index];
if (element) {
await _uploadImage(element, { shouldInsert });
}
}
} catch (error) {
console.log('Failed to upload image', error);
bugsnapInstance.notify(error);
}
};
const _uploadImage = async (media, { shouldInsert } = { shouldInsert: false }) => {
if (!isLoggedIn) return;
try {
if (setIsUploading) {
setIsUploading(true);
}
if (!shouldInsert) {
setIsAddingToUploads(true);
}
await mediaUploadMutation.mutateAsync({
media,
addToUploads: !shouldInsert
}, {
onSuccess: (data) => {
console.log('upload successfully', data, media, shouldInsert)
if (data && data.url && shouldInsert) {
_handleMediaInsertion({
filename: media.filename,
url: data.url,
text: '',
status: MediaInsertStatus.READY,
});
}
},
onSettled: () => {
if (setIsUploading) {
setIsUploading(false);
}
setIsAddingToUploads(false);
},
onError: (err) => { throw err },
})
} catch (error) {
console.log('error while uploading image : ', error);
if (error.toString().includes('code 413')) {
Alert.alert(
intl.formatMessage({
id: 'alert.fail',
}),
intl.formatMessage({
id: 'alert.payloadTooLarge',
}),
);
} else if (error.toString().includes('code 429')) {
Alert.alert(
intl.formatMessage({
id: 'alert.fail',
}),
intl.formatMessage({
id: 'alert.quotaExceeded',
}),
);
} else if (error.toString().includes('code 400')) {
Alert.alert(
intl.formatMessage({
id: 'alert.fail',
}),
intl.formatMessage({
id: 'alert.invalidImage',
}),
);
} else {
Alert.alert(
intl.formatMessage({
id: 'alert.fail',
}),
error.message || error.toString(),
);
}
if (shouldInsert) {
_handleMediaInsertion({
filename: media.filename,
url: '',
text: '',
status: MediaInsertStatus.FAILED,
});
}
}
};
const _handleMediaOnSelectFailure = (error) => {
if (error.code === 'E_PERMISSION_MISSING') {
Alert.alert(
intl.formatMessage({
id: 'alert.permission_denied',
}),
intl.formatMessage({
id: 'alert.permission_text',
}),
);
} else {
Alert.alert(
intl.formatMessage({
id: 'alert.fail',
}),
error.message || JSON.stringify(error),
);
}
};
const _handleMediaInsertion = (data: MediaInsertData) => {
if (isEditing) {
pendingInserts.current.push(data);
} else if (handleMediaInsert) {
handleMediaInsert([data]);
}
};
//fetch images from server
const _getMediaUploads = async () => {
try {
if (username) {
console.log('getting images for: ' + username);
const images = await getImages();
console.log('images received', images);
setMediaUploads(images || []);
}
} catch (err) {
console.warn('Failed to get images');
}
setIsAddingToUploads(false);
};
//inserts media items in post body
const _insertMedia = async (map: Map<number, boolean>) => {
const data: MediaInsertData[] = [];
for (const index of map.keys()) {
console.log(index);
const item = mediaUploads[index];
data.push({
url: item.url,
text: '',
status: MediaInsertStatus.READY,
});
}
handleMediaInsert(data);
};
return (
!isPreviewActive &&
showModal && (
<UploadsGalleryContent
insertedMediaUrls={insertedMediaUrls}
mediaUploads={mediaQuery.data.slice()}
isAddingToUploads={isAddingToUploads}
getMediaUploads={_getMediaUploads}
insertMedia={_insertMedia}
handleOpenCamera={_handleOpenCamera}
handleOpenGallery={_handleOpenImagePicker}
/>
)
onSettled: () => {
if (setIsUploading) {
setIsUploading(false);
}
setIsAddingToUploads(false);
},
onError: (err) => {
throw err;
},
},
);
},
} catch (error) {
console.log('error while uploading image : ', error);
if (error.toString().includes('code 413')) {
Alert.alert(
intl.formatMessage({
id: 'alert.fail',
}),
intl.formatMessage({
id: 'alert.payloadTooLarge',
}),
);
} else if (error.toString().includes('code 429')) {
Alert.alert(
intl.formatMessage({
id: 'alert.fail',
}),
intl.formatMessage({
id: 'alert.quotaExceeded',
}),
);
} else if (error.toString().includes('code 400')) {
Alert.alert(
intl.formatMessage({
id: 'alert.fail',
}),
intl.formatMessage({
id: 'alert.invalidImage',
}),
);
} else {
Alert.alert(
intl.formatMessage({
id: 'alert.fail',
}),
error.message || error.toString(),
);
}
if (shouldInsert) {
_handleMediaInsertion({
filename: media.filename,
url: '',
text: '',
status: MediaInsertStatus.FAILED,
});
}
}
};
const _handleMediaOnSelectFailure = (error) => {
if (error.code === 'E_PERMISSION_MISSING') {
Alert.alert(
intl.formatMessage({
id: 'alert.permission_denied',
}),
intl.formatMessage({
id: 'alert.permission_text',
}),
);
} else {
Alert.alert(
intl.formatMessage({
id: 'alert.fail',
}),
error.message || JSON.stringify(error),
);
}
};
const _handleMediaInsertion = (data: MediaInsertData) => {
if (isEditing) {
pendingInserts.current.push(data);
} else if (handleMediaInsert) {
handleMediaInsert([data]);
}
};
//fetch images from server
const _getMediaUploads = async () => {
try {
if (username) {
console.log('getting images for: ' + username);
const images = await getImages();
console.log('images received', images);
setMediaUploads(images || []);
}
} catch (err) {
console.warn('Failed to get images');
}
setIsAddingToUploads(false);
};
//inserts media items in post body
const _insertMedia = async (map: Map<number, boolean>) => {
const data: MediaInsertData[] = [];
for (const index of map.keys()) {
console.log(index);
const item = mediaUploads[index];
data.push({
url: item.url,
text: '',
status: MediaInsertStatus.READY,
});
}
handleMediaInsert(data);
};
return (
!isPreviewActive &&
showModal && (
<UploadsGalleryContent
insertedMediaUrls={insertedMediaUrls}
mediaUploads={mediaQuery.data.slice()}
isAddingToUploads={isAddingToUploads}
getMediaUploads={_getMediaUploads}
insertMedia={_insertMedia}
handleOpenCamera={_handleOpenCamera}
handleOpenGallery={_handleOpenImagePicker}
/>
)
);
},
);

View File

@ -140,8 +140,8 @@ const UpvoteView = ({
//record user points
userActivityMutation.mutate({
pointsTy: PointActivityIds.VOTE,
transactionId: response.id
})
transactionId: response.id,
});
if (!response || !response.id) {
Alert.alert(
@ -214,8 +214,8 @@ const UpvoteView = ({
//record usr points
userActivityMutation.mutate({
pointsTy: PointActivityIds.VOTE,
transactionId: response.id
})
transactionId: response.id,
});
setUpvote(!!sliderValue);
setIsVoting(false);
onVote(amount, true);

View File

@ -8,5 +8,4 @@ const coingeckoApi = axios.create({
baseURL: `${BASE_URL}/${PATH_API}/${API_VERSION}`,
});
export default coingeckoApi;
export default coingeckoApi;

View File

@ -5,7 +5,7 @@ import { get } from 'lodash';
import { store } from '../redux/store/store';
import { getDigitPinCode } from '../providers/hive/dhive';
import { decryptKey } from '../utils/crypto';
import bugsnagInstance from '../config/bugsnag';
import bugsnagInstance from './bugsnag';
const api = axios.create({
baseURL: Config.ECENCY_BACKEND_API,
@ -19,16 +19,17 @@ api.interceptors.request.use((request) => {
console.log('Starting ecency Request', request);
//skip code addition is register and token refresh endpoint is triggered
if (request.url === '/private-api/account-create'
|| request.url === '/auth-api/hs-token-refresh'
|| request.url === '/private-api/promoted-entries'
|| request.url.startsWith('private-api/leaderboard')
|| request.url.startsWith('/private-api/received-vesting/')
|| request.url.startsWith('/private-api/referrals/')
|| request.url.startsWith('/private-api/market-data')
|| request.url.startsWith('/private-api/comment-history')
if (
request.url === '/private-api/account-create' ||
request.url === '/auth-api/hs-token-refresh' ||
request.url === '/private-api/promoted-entries' ||
request.url.startsWith('private-api/leaderboard') ||
request.url.startsWith('/private-api/received-vesting/') ||
request.url.startsWith('/private-api/referrals/') ||
request.url.startsWith('/private-api/market-data') ||
request.url.startsWith('/private-api/comment-history')
) {
return request
return request;
}
if (!request.data?.code) {
@ -47,12 +48,15 @@ api.interceptors.request.use((request) => {
console.log('Added access token:', accessToken);
} else {
const isLoggedIn = state.application.isLoggedIn;
console.warn("Failed to inject accessToken", `isLoggedIn:${isLoggedIn}`)
bugsnagInstance.notify(new Error(`Failed to inject accessToken in ${request.url} call. isLoggedIn:${isLoggedIn}, local.acccessToken:${token}, pin:${pin}`))
console.warn('Failed to inject accessToken', `isLoggedIn:${isLoggedIn}`);
bugsnagInstance.notify(
new Error(
`Failed to inject accessToken in ${request.url} call. isLoggedIn:${isLoggedIn}, local.acccessToken:${token}, pin:${pin}`,
),
);
}
}
return request;
});

View File

@ -1,29 +1,33 @@
import { CoinBase } from "../redux/reducers/walletReducer"
import { CoinBase } from '../redux/reducers/walletReducer';
const DEFAULT_COINS = [{
id:'ecency',
symbol:'Points',
notCrypto:true
},{
id:'hive_power',
symbol:'HP',
notCrypto:true
},{
id:'hive',
symbol:'HIVE',
notCrypto:false
},{
id:'hive_dollar',
symbol:'HBD',
notCrypto:false
}] as CoinBase[]
const DEFAULT_COINS = [
{
id: 'ecency',
symbol: 'Points',
notCrypto: true,
},
{
id: 'hive_power',
symbol: 'HP',
notCrypto: true,
},
{
id: 'hive',
symbol: 'HIVE',
notCrypto: false,
},
{
id: 'hive_dollar',
symbol: 'HBD',
notCrypto: false,
},
] as CoinBase[];
export const COIN_IDS = {
ECENCY:'ecency',
HIVE:'hive',
HBD:'hive_dollar',
HP:'hive_power'
}
ECENCY: 'ecency',
HIVE: 'hive',
HBD: 'hive_dollar',
HP: 'hive_power',
};
export default DEFAULT_COINS
export default DEFAULT_COINS;

View File

@ -1,6 +1,5 @@
export default [
{key:'settings.theme.system', value: null},
{key:'settings.theme.light', value: false},
{key:'settings.theme.dark', value: true}
];
{ key: 'settings.theme.system', value: null },
{ key: 'settings.theme.light', value: false },
{ key: 'settings.theme.dark', value: true },
];

View File

@ -4,5 +4,5 @@ const DELETE_ACCOUNT = 'delete_account';
export default {
SHOW_HIDE_IMGS,
DELETE_ACCOUNT
DELETE_ACCOUNT,
};

View File

@ -10,7 +10,7 @@ const DELEGATE = 'delegate';
const POWER_DOWN = 'power_down';
const ADDRESS_VIEW = 'address_view';
const DELEGATE_VESTING_SHARES = 'delegate_vesting_shares';
const WITHDRAW_VESTING = 'withdraw_vesting'
const WITHDRAW_VESTING = 'withdraw_vesting';
export default {
TRANSFER_TOKEN,
@ -25,5 +25,5 @@ export default {
POWER_DOWN,
ADDRESS_VIEW,
DELEGATE_VESTING_SHARES,
WITHDRAW_VESTING
WITHDRAW_VESTING,
};

View File

@ -47,18 +47,15 @@ class InAppPurchaseContainer extends Component {
RNIap.endConnection();
}
_initContainer = async () => {
const {
intl,
} = this.props;
const { intl } = this.props;
try {
await RNIap.initConnection();
if (Platform.OS === 'android') {
await RNIap.flushFailedPurchasesCachedAsPendingAndroid();
await RNIap.flushFailedPurchasesCachedAsPendingAndroid();
}
await this._consumeAvailablePurchases()
await this._consumeAvailablePurchases();
this._getItems();
this._purchaseUpdatedListener();
await this._handleQrPurchase();
@ -70,31 +67,28 @@ class InAppPurchaseContainer extends Component {
intl.formatMessage({
id: 'alert.connection_issues',
}),
err.message
err.message,
);
}
}
};
//this snippet consumes all previously bought purchases
//this snippet consumes all previously bought purchases
//that are set to be consumed yet
_consumeAvailablePurchases = async () => {
try{
try {
//get available purchase
const purchases = await RNIap.getAvailablePurchases();
//check consumeable status
for (let i = 0; i < purchases.length; i++) {
//consume item using finishTransactionx
//consume item using finishTransactionx
await RNIap.finishTransaction(purchases[i], true);
}
}catch(err){
} catch (err) {
bugsnagInstance.notify(err);
console.warn(err.code, err.message);
}
}
};
// Component Functions
_purchaseUpdatedListener = () => {
@ -157,7 +151,7 @@ class InAppPurchaseContainer extends Component {
intl.formatMessage({
id: 'alert.warning',
}),
error.message
error.message,
);
}
this.setState({ isProcessing: false });
@ -169,7 +163,7 @@ class InAppPurchaseContainer extends Component {
if (_title !== 'FREE POINTS') {
_title = _title.replace(/[^0-9]+/g, '') + ' POINTS';
}
return _title;
};
@ -183,11 +177,11 @@ class InAppPurchaseContainer extends Component {
} catch (err) {
bugsnagInstance.notify(err);
Alert.alert(
intl.formatMessage({
id: 'alert.connection_issues',
}),
error.message
);
intl.formatMessage({
id: 'alert.connection_issues',
}),
error.message,
);
}
this.setState({ isLoading: false });
@ -219,23 +213,29 @@ class InAppPurchaseContainer extends Component {
const productId = navigation.getParam('productId', '');
const username = navigation.getParam('username', '');
const product:Product = productId && products && products.find((product) => product.productId === productId)
const product: Product =
productId && products && products.find((product) => product.productId === productId);
if (product) {
let body = intl.formatMessage({
id: 'boost.confirm_purchase_summary'
}, {
let body = intl.formatMessage(
{
id: 'boost.confirm_purchase_summary',
},
{
points: this._getTitle(product.title),
username,
price: `${product.currency} ${product.price}`
});
username,
price: `${product.currency} ${product.price}`,
},
);
let title = intl.formatMessage({
id: 'boost.confirm_purchase'
}, {
username,
});
let title = intl.formatMessage(
{
id: 'boost.confirm_purchase',
},
{
username,
},
);
dispatch(
showActionModal({
@ -251,13 +251,12 @@ class InAppPurchaseContainer extends Component {
onPress: async () => await this._buyItem(productId),
},
],
headerContent: <UserAvatar username={username} size='xl' />,
headerContent: <UserAvatar username={username} size="xl" />,
}),
);
}
}
};
render() {
const { children, isNoSpin, navigation } = this.props;
const { productList, isLoading, isProcessing } = this.state;
@ -276,7 +275,7 @@ class InAppPurchaseContainer extends Component {
getItems: this._getItems,
getTitle: this._getTitle,
spinProduct: productList.filter((item) => item.productId.includes('spins')),
navigation: navigation
navigation: navigation,
})
);
}

View File

@ -10,22 +10,19 @@ import { setTopLevelNavigator } from './service';
import ROUTES from '../constants/routeNames';
import parseVersionNumber from '../utils/parseVersionNumber';
export const AppNavigator = () => {
const lastAppVersion = useAppSelector(state => state.application.lastAppVersion)
const lastAppVersion = useAppSelector((state) => state.application.lastAppVersion);
const [appVersion] = useState(VersionNumber.appVersion);
const _initRoute = (!lastAppVersion || (parseVersionNumber(lastAppVersion) < parseVersionNumber(appVersion))) ?
ROUTES.SCREENS.WELCOME : ROUTES.SCREENS.FEED;
const _initRoute =
!lastAppVersion || parseVersionNumber(lastAppVersion) < parseVersionNumber(appVersion)
? ROUTES.SCREENS.WELCOME
: ROUTES.SCREENS.FEED;
return (
<NavigationContainer ref={ref=>setTopLevelNavigator(ref)}>
<NavigationContainer ref={(ref) => setTopLevelNavigator(ref)}>
<StackNavigator initRoute={_initRoute} />
</NavigationContainer>
)
}
);
};

View File

@ -1,75 +1,65 @@
import React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import ROUTES from '../constants/routeNames';
import { BottomTabBar } from '../components';
import {
Feed,
Notification,
Profile,
Wallet,
} from '../screens';
import { Feed, Notification, Profile, Wallet } from '../screens';
const Tab = createBottomTabNavigator();
export const BottomTabNavigator = () => {
return (
<Tab.Navigator
tabBar={(props) => <BottomTabBar {...props} />}
backBehavior="initialRoute"
screenOptions={{
headerShown: false,
tabBarShowLabel: false,
tabBarActiveTintColor: '#357ce6',
tabBarInactiveTintColor: '#c1c5c7',
}}
>
<Tab.Screen
name={ROUTES.TABBAR.FEED}
component={Feed}
initialParams={{
iconName: 'view-day', //read in bottomTabBarView
}}
/>
<Tab.Screen
name={ROUTES.TABBAR.NOTIFICATION}
component={Notification}
initialParams={{
iconName: 'notifications', //read in bottomTabBarView
}}
/>
return (
<Tab.Navigator
tabBar={(props) => <BottomTabBar {...props} /> }
<Tab.Screen
name={ROUTES.TABBAR.POST_BUTTON}
component={EmptyScreen}
initialParams={{
iconName: 'pencil', //read in bottomTabBarView
}}
/>
backBehavior='initialRoute'
screenOptions={{
headerShown:false,
tabBarShowLabel: false,
tabBarActiveTintColor: '#357ce6',
tabBarInactiveTintColor: '#c1c5c7',}}
>
<Tab.Screen
name={ROUTES.TABBAR.FEED}
component={Feed}
initialParams={{
iconName: 'view-day' //read in bottomTabBarView
}}
/>
<Tab.Screen
name={ROUTES.TABBAR.WALLET}
component={Wallet}
initialParams={{
iconName: 'account-balance-wallet', //read in bottomTabBarView
}}
/>
<Tab.Screen
name={ROUTES.TABBAR.NOTIFICATION}
component={Notification}
initialParams={{
iconName: 'notifications' //read in bottomTabBarView
}}
/>
<Tab.Screen
name={ROUTES.TABBAR.PROFILE}
component={Profile}
initialParams={{
iconName: 'person', //read in bottomTabBarView
}}
/>
</Tab.Navigator>
);
};
<Tab.Screen
name={ROUTES.TABBAR.POST_BUTTON}
component={EmptyScreen}
initialParams={{
iconName: 'pencil' //read in bottomTabBarView
}}
/>
<Tab.Screen
name={ROUTES.TABBAR.WALLET}
component={Wallet}
initialParams={{
iconName: 'account-balance-wallet' //read in bottomTabBarView
}}
/>
<Tab.Screen
name={ROUTES.TABBAR.PROFILE}
component={Profile}
initialParams={{
iconName: 'person' //read in bottomTabBarView
}}
/>
</Tab.Navigator>
)
}
const EmptyScreen = () => null
const EmptyScreen = () => null;

View File

@ -1,19 +1,20 @@
import React from 'react';
import { SideMenu } from "../components"
import { BottomTabNavigator } from "./botomTabNavigator"
import { createDrawerNavigator } from '@react-navigation/drawer';
import { SideMenu } from '../components';
import { BottomTabNavigator } from './botomTabNavigator';
// Constants
import ROUTES from '../constants/routeNames';
import { createDrawerNavigator } from "@react-navigation/drawer";
const Drawer = createDrawerNavigator();
export const DrawerNavigator = () => {
return (
<Drawer.Navigator screenOptions={{headerShown:false}} drawerContent={(props) => <SideMenu {...props}/>} >
<Drawer.Screen name={ROUTES.SCREENS.FEED} component={BottomTabNavigator} />
</Drawer.Navigator>
)
}
return (
<Drawer.Navigator
screenOptions={{ headerShown: false }}
drawerContent={(props) => <SideMenu {...props} />}
>
<Drawer.Screen name={ROUTES.SCREENS.FEED} component={BottomTabNavigator} />
</Drawer.Navigator>
);
};

View File

@ -1 +1 @@
export * from './appNavigator';
export * from './appNavigator';

View File

@ -1,94 +1,95 @@
import React from 'react';
// Constants
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import ROUTES from '../constants/routeNames';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
// Screens
import {
Bookmarks,
Boost,
Drafts,
Editor,
Follows,
Login,
Post,
Profile,
ProfileEdit,
Reblogs,
Redeem,
Register,
SearchResult,
Settings,
SpinGame,
Transfer,
Voters,
AccountBoost,
TagResult,
Community,
Communities,
WebBrowser,
ReferScreen,
CoinDetails,
EditHistoryScreen,
WelcomeScreen,
PinCode,
Bookmarks,
Boost,
Drafts,
Editor,
Follows,
Login,
Post,
Profile,
ProfileEdit,
Reblogs,
Redeem,
Register,
SearchResult,
Settings,
SpinGame,
Transfer,
Voters,
AccountBoost,
TagResult,
Community,
Communities,
WebBrowser,
ReferScreen,
CoinDetails,
EditHistoryScreen,
WelcomeScreen,
PinCode,
} from '../screens';
import { DrawerNavigator } from './drawerNavigator';
const RootStack = createNativeStackNavigator();
const MainStack = createNativeStackNavigator();
const MainStackNavigator = () => {
return (
<MainStack.Navigator screenOptions={{ headerShown: false, animation: 'slide_from_right' }} >
<MainStack.Screen name={ROUTES.DRAWER.MAIN} component={DrawerNavigator} />
<MainStack.Screen name={ROUTES.SCREENS.PROFILE} component={Profile} />
<MainStack.Screen name={ROUTES.SCREENS.PROFILE_EDIT} component={ProfileEdit} />
<MainStack.Screen name={ROUTES.SCREENS.SETTINGS} component={Settings} />
<MainStack.Screen name={ROUTES.SCREENS.DRAFTS} component={Drafts} />
<MainStack.Screen name={ROUTES.SCREENS.BOOKMARKS} component={Bookmarks} />
<MainStack.Screen name={ROUTES.SCREENS.SEARCH_RESULT} component={SearchResult} />
<MainStack.Screen name={ROUTES.SCREENS.TAG_RESULT} component={TagResult} />
<MainStack.Screen name={ROUTES.SCREENS.BOOST} component={Boost} />
<MainStack.Screen name={ROUTES.SCREENS.REDEEM} component={Redeem} />
<MainStack.Screen name={ROUTES.SCREENS.SPIN_GAME} component={SpinGame} />
<MainStack.Screen name={ROUTES.SCREENS.ACCOUNT_BOOST} component={AccountBoost} />
<MainStack.Screen name={ROUTES.SCREENS.COMMUNITY} component={Community} />
<MainStack.Screen name={ROUTES.SCREENS.COMMUNITIES} component={Communities} />
<MainStack.Screen name={ROUTES.SCREENS.WEB_BROWSER} component={WebBrowser} />
<MainStack.Screen name={ROUTES.SCREENS.REFER} component={ReferScreen} />
<MainStack.Screen name={ROUTES.SCREENS.COIN_DETAILS} component={CoinDetails} />
<MainStack.Screen name={ROUTES.SCREENS.EDIT_HISTORY} component={EditHistoryScreen} />
<MainStack.Screen name={ROUTES.SCREENS.POST} component={Post} />
<MainStack.Group screenOptions={{ animation: 'slide_from_bottom' }}>
<MainStack.Screen name={ROUTES.SCREENS.REBLOGS} component={Reblogs} />
<MainStack.Screen name={ROUTES.SCREENS.VOTERS} component={Voters} />
<MainStack.Screen name={ROUTES.SCREENS.FOLLOWS} component={Follows} />
<MainStack.Screen name={ROUTES.SCREENS.TRANSFER} component={Transfer} />
<MainStack.Screen name={ROUTES.SCREENS.EDITOR} component={Editor} />
</MainStack.Group>
</MainStack.Navigator>
)
}
return (
<MainStack.Navigator screenOptions={{ headerShown: false, animation: 'slide_from_right' }}>
<MainStack.Screen name={ROUTES.DRAWER.MAIN} component={DrawerNavigator} />
<MainStack.Screen name={ROUTES.SCREENS.PROFILE} component={Profile} />
<MainStack.Screen name={ROUTES.SCREENS.PROFILE_EDIT} component={ProfileEdit} />
<MainStack.Screen name={ROUTES.SCREENS.SETTINGS} component={Settings} />
<MainStack.Screen name={ROUTES.SCREENS.DRAFTS} component={Drafts} />
<MainStack.Screen name={ROUTES.SCREENS.BOOKMARKS} component={Bookmarks} />
<MainStack.Screen name={ROUTES.SCREENS.SEARCH_RESULT} component={SearchResult} />
<MainStack.Screen name={ROUTES.SCREENS.TAG_RESULT} component={TagResult} />
<MainStack.Screen name={ROUTES.SCREENS.BOOST} component={Boost} />
<MainStack.Screen name={ROUTES.SCREENS.REDEEM} component={Redeem} />
<MainStack.Screen name={ROUTES.SCREENS.SPIN_GAME} component={SpinGame} />
<MainStack.Screen name={ROUTES.SCREENS.ACCOUNT_BOOST} component={AccountBoost} />
<MainStack.Screen name={ROUTES.SCREENS.COMMUNITY} component={Community} />
<MainStack.Screen name={ROUTES.SCREENS.COMMUNITIES} component={Communities} />
<MainStack.Screen name={ROUTES.SCREENS.WEB_BROWSER} component={WebBrowser} />
<MainStack.Screen name={ROUTES.SCREENS.REFER} component={ReferScreen} />
<MainStack.Screen name={ROUTES.SCREENS.COIN_DETAILS} component={CoinDetails} />
<MainStack.Screen name={ROUTES.SCREENS.EDIT_HISTORY} component={EditHistoryScreen} />
<MainStack.Screen name={ROUTES.SCREENS.POST} component={Post} />
<MainStack.Group screenOptions={{ animation: 'slide_from_bottom' }}>
<MainStack.Screen name={ROUTES.SCREENS.REBLOGS} component={Reblogs} />
<MainStack.Screen name={ROUTES.SCREENS.VOTERS} component={Voters} />
<MainStack.Screen name={ROUTES.SCREENS.FOLLOWS} component={Follows} />
<MainStack.Screen name={ROUTES.SCREENS.TRANSFER} component={Transfer} />
<MainStack.Screen name={ROUTES.SCREENS.EDITOR} component={Editor} />
</MainStack.Group>
</MainStack.Navigator>
);
};
export const StackNavigator = ({initRoute}) => {
export const StackNavigator = ({ initRoute }) => {
return (
<RootStack.Navigator
initialRouteName={initRoute}
screenOptions={{ headerShown: false, animation: 'slide_from_bottom' }}
return (
<RootStack.Navigator
<RootStack.Screen name={ROUTES.STACK.MAIN} component={MainStackNavigator} />
initialRouteName={initRoute}
screenOptions={{ headerShown: false, animation:'slide_from_bottom' }}>
<RootStack.Screen name={ROUTES.STACK.MAIN} component={MainStackNavigator} />
<RootStack.Screen name={ROUTES.SCREENS.REGISTER} component={Register} />
<RootStack.Screen name={ROUTES.SCREENS.LOGIN} component={Login} />
<RootStack.Screen name={ROUTES.SCREENS.WELCOME} component={WelcomeScreen}/>
<RootStack.Screen name={ROUTES.SCREENS.PINCODE} options={{gestureEnabled:false}} component={PinCode}/>
</RootStack.Navigator>
)
}
<RootStack.Screen name={ROUTES.SCREENS.REGISTER} component={Register} />
<RootStack.Screen name={ROUTES.SCREENS.LOGIN} component={Login} />
<RootStack.Screen name={ROUTES.SCREENS.WELCOME} component={WelcomeScreen} />
<RootStack.Screen
name={ROUTES.SCREENS.PINCODE}
options={{ gestureEnabled: false }}
component={PinCode}
/>
</RootStack.Navigator>
);
};

View File

@ -3,7 +3,6 @@ import coingeckoApi from '../../config/coingeckoApi';
import { convertMarketData } from './converters';
import { MarketData } from './models';
const PATH_COINS = 'coins';
const PATH_MARKET_CHART = 'market_chart';
@ -11,32 +10,31 @@ export const INTERVAL_HOURLY = 'hourly';
export const INTERVAL_DAILY = 'daily';
export const fetchMarketChart = async (
coingeckoId:string,
vs_currency:string,
days:number,
interval:'daily'|'hourly' = 'daily'
): Promise<MarketData> => {
try{
const params = {
vs_currency,
days,
interval
}
coingeckoId: string,
vs_currency: string,
days: number,
interval: 'daily' | 'hourly' = 'daily',
): Promise<MarketData> => {
try {
const params = {
vs_currency,
days,
interval,
};
const res = await coingeckoApi.get(`/${PATH_COINS}/${coingeckoId}/${PATH_MARKET_CHART}`, {
params
});
const rawData = res.data;
if(!rawData){
throw new Error("Tag name not available")
}
const res = await coingeckoApi.get(`/${PATH_COINS}/${coingeckoId}/${PATH_MARKET_CHART}`, {
params,
});
const rawData = res.data;
if (!rawData) {
throw new Error('Tag name not available');
}
const data = convertMarketData(rawData);
const data = convertMarketData(rawData);
return data;
}catch(error){
bugsnagInstance.notify(error);
throw error;
}
}
return data;
} catch (error) {
bugsnagInstance.notify(error);
throw error;
}
};

View File

@ -1,23 +1,23 @@
import { ChartItem, MarketData } from "./models"
import { ChartItem, MarketData } from './models';
export const convertChartItem = (rawData:any) => {
if(!rawData){
return null;
}
return {
xValue:rawData[0],
yValue:rawData[1]
} as ChartItem;
}
export const convertChartItem = (rawData: any) => {
if (!rawData) {
return null;
}
return {
xValue: rawData[0],
yValue: rawData[1],
} as ChartItem;
};
export const convertMarketData = (rawData:any) => {
if(!rawData){
return null;
}
export const convertMarketData = (rawData: any) => {
if (!rawData) {
return null;
}
return {
prices:rawData.prices ? rawData.prices.map(convertChartItem) : [],
marketCaps:rawData.market_caps ? rawData.market_caps.map(convertChartItem) : [],
totalVolumes:rawData.total_volumes ? rawData.total_volumes.map(convertChartItem) : []
} as MarketData;
}
return {
prices: rawData.prices ? rawData.prices.map(convertChartItem) : [],
marketCaps: rawData.market_caps ? rawData.market_caps.map(convertChartItem) : [],
totalVolumes: rawData.total_volumes ? rawData.total_volumes.map(convertChartItem) : [],
} as MarketData;
};

View File

@ -1,10 +1,10 @@
export interface ChartItem {
xValue:number,
yValue:number
xValue: number;
yValue: number;
}
export interface MarketData {
prices:Array<ChartItem>;
marketCaps:Array<ChartItem>;
totalVolumes:Array<ChartItem>;
}
prices: Array<ChartItem>;
marketCaps: Array<ChartItem>;
totalVolumes: Array<ChartItem>;
}

View File

@ -1,6 +1,12 @@
import { COIN_IDS } from '../../constants/defaultCoins';
import { Referral } from '../../models';
import { CommentHistoryItem, LatestMarketPrices, LatestQuotes, QuoteItem, ReferralStat } from './ecency.types';
import {
CommentHistoryItem,
LatestMarketPrices,
LatestQuotes,
QuoteItem,
ReferralStat,
} from './ecency.types';
export const convertReferral = (rawData: any) => {
return {
@ -19,23 +25,23 @@ export const convertReferralStat = (rawData: any) => {
} as ReferralStat;
};
export const convertQuoteItem = (rawData:any, currencyRate:number) => {
if(!rawData){
export const convertQuoteItem = (rawData: any, currencyRate: number) => {
if (!rawData) {
return null;
}
return {
price:rawData.price * currencyRate,
percentChange:rawData.percent_change,
lastUpdated:rawData.last_updated,
} as QuoteItem
}
price: rawData.price * currencyRate,
percentChange: rawData.percent_change,
lastUpdated: rawData.last_updated,
} as QuoteItem;
};
export const convertLatestQuotes = (rawData: any, currencyRate:number) => {
export const convertLatestQuotes = (rawData: any, currencyRate: number) => {
return {
[COIN_IDS.HIVE]:convertQuoteItem(rawData.hive.quotes.usd, currencyRate),
[COIN_IDS.HP]:convertQuoteItem(rawData.hive.quotes.usd, currencyRate),
[COIN_IDS.HBD]:convertQuoteItem(rawData.hbd.quotes.usd, currencyRate),
[COIN_IDS.ECENCY]:convertQuoteItem(rawData.estm.quotes.usd, currencyRate)
[COIN_IDS.HIVE]: convertQuoteItem(rawData.hive.quotes.usd, currencyRate),
[COIN_IDS.HP]: convertQuoteItem(rawData.hive.quotes.usd, currencyRate),
[COIN_IDS.HBD]: convertQuoteItem(rawData.hbd.quotes.usd, currencyRate),
[COIN_IDS.ECENCY]: convertQuoteItem(rawData.estm.quotes.usd, currencyRate),
} as LatestQuotes;
};
@ -47,4 +53,4 @@ export const convertCommentHistory = (rawData: any) => {
title: rawData.title || '',
v: rawData.v || 1,
} as CommentHistoryItem;
};
};

View File

@ -4,18 +4,16 @@ import ecencyApi from '../../config/ecencyApi';
import bugsnagInstance from '../../config/bugsnag';
import { EcencyUser, UserPoint } from './ecency.types';
/**
* Records user activty and reward poinsts
* @param ty points
* @param bl block number
* @param tx transaction id
* @returns
* @returns
*/
export const userActivity = async (ty: number, tx: string = '', bl: string | number = '') => {
try {
const data: {
ty: number;
bl?: string | number;
tx?: string | number;
@ -24,28 +22,27 @@ export const userActivity = async (ty: number, tx: string = '', bl: string | num
if (bl) data.bl = bl;
if (tx) data.tx = tx;
const response = await ecencyApi.post('/private-api/usr-activity', data)
const response = await ecencyApi.post('/private-api/usr-activity', data);
return response.data;
} catch (error) {
console.warn("Failed to push user activity point", error);
bugsnagInstance.notify(error)
throw error
}
}
export const getPointsSummary = async (username:string): Promise<EcencyUser> => {
try {
const data = {username};
const response = await ecencyApi.post('/private-api/points', data);
console.log("returning user points data", response.data);
return response.data;
} catch (error) {
console.warn("Failed to get points", error);
console.warn('Failed to push user activity point', error);
bugsnagInstance.notify(error);
throw new Error(error.response?.data?.message || error.message)
throw error;
}
}
};
export const getPointsSummary = async (username: string): Promise<EcencyUser> => {
try {
const data = { username };
const response = await ecencyApi.post('/private-api/points', data);
console.log('returning user points data', response.data);
return response.data;
} catch (error) {
console.warn('Failed to get points', error);
bugsnagInstance.notify(error);
throw new Error(error.response?.data?.message || error.message);
}
};
export const getPointsHistory = (username: string): Promise<UserPoint[]> =>
new Promise((resolve) => {
@ -60,18 +57,16 @@ export const getPointsHistory = (username: string): Promise<UserPoint[]> =>
});
});
export const claimPoints = async () => {
try {
const response = await ecencyApi.post('/private-api/points-claim')
const response = await ecencyApi.post('/private-api/points-claim');
return response.data;
} catch (error) {
console.warn("Failed to calim points", error);
console.warn('Failed to calim points', error);
bugsnagInstance.notify(error);
throw new Error(error.response?.data?.message || error.message)
throw new Error(error.response?.data?.message || error.message);
}
}
};
export const gameStatusCheck = (username, type) =>
new Promise((resolve, reject) => {

View File

@ -66,10 +66,10 @@ export interface CommentHistoryItem {
}
export interface PointActivity {
pointsTy:number;
username?:string;
transactionId?:string;
blockNum?:number|string;
pointsTy: number;
username?: string;
transactionId?: string;
blockNum?: number | string;
}
export enum ScheduledPostStatus {
@ -80,18 +80,17 @@ export enum ScheduledPostStatus {
}
export enum NotificationFilters {
ACTIVITIES = "activities",
RVOTES = "rvotes",
MENTIONS = "mentions",
FOLLOWS = "follows",
REPLIES = "replies",
REBLOGS = "reblogs",
TRANFERS = "transfers",
DELEGATIONS = "delegations",
FAVOURITES = "nfavorites"
ACTIVITIES = 'activities',
RVOTES = 'rvotes',
MENTIONS = 'mentions',
FOLLOWS = 'follows',
REPLIES = 'replies',
REBLOGS = 'reblogs',
TRANFERS = 'transfers',
DELEGATIONS = 'delegations',
FAVOURITES = 'nfavorites',
}
export enum PointActivityIds {
VIEW_POST = 10,
LOGIN = 20,

View File

@ -1,113 +1,111 @@
export interface Vote {
percent: number;
reputation: number;
rshares: string;
time: string;
timestamp?: number;
voter: string;
weight: number;
reward?: number;
percent: number;
reputation: number;
rshares: string;
time: string;
timestamp?: number;
voter: string;
weight: number;
reward?: number;
}
export interface DynamicGlobalProperties {
hbd_print_rate: number;
total_vesting_fund_hive: string;
total_vesting_shares: string;
hbd_interest_rate: number;
head_block_number: number;
vesting_reward_percent: number;
virtual_supply: string;
hbd_print_rate: number;
total_vesting_fund_hive: string;
total_vesting_shares: string;
hbd_interest_rate: number;
head_block_number: number;
vesting_reward_percent: number;
virtual_supply: string;
}
export interface FeedHistory {
current_median_history: {
base: string;
quote: string;
};
current_median_history: {
base: string;
quote: string;
};
}
export interface RewardFund {
recent_claims: string;
reward_balance: string;
recent_claims: string;
reward_balance: string;
}
export interface DelegatedVestingShare {
id: number;
delegatee: string;
delegator: string;
min_delegation_time: string;
vesting_shares: string;
id: number;
delegatee: string;
delegator: string;
min_delegation_time: string;
vesting_shares: string;
}
export interface Follow {
follower: string;
following: string;
what: string[];
follower: string;
following: string;
what: string[];
}
export interface MarketStatistics {
hbd_volume: string;
highest_bid: string;
hive_volume: string;
latest: string;
lowest_ask: string;
percent_change: string;
hbd_volume: string;
highest_bid: string;
hive_volume: string;
latest: string;
lowest_ask: string;
percent_change: string;
}
export interface OpenOrderItem {
id: number,
created: string,
expiration: string,
seller: string,
orderid: number,
for_sale: number,
sell_price: {
base: string,
quote: string
},
real_price: string,
rewarded: boolean
id: number;
created: string;
expiration: string;
seller: string;
orderid: number;
for_sale: number;
sell_price: {
base: string;
quote: string;
};
real_price: string;
rewarded: boolean;
}
export interface OrdersDataItem {
created: string;
hbd: number;
hive: number;
order_price: {
base: string;
quote: string;
}
real_price: string;
created: string;
hbd: number;
hive: number;
order_price: {
base: string;
quote: string;
};
real_price: string;
}
export interface TradeDataItem {
current_pays: string;
date: number;
open_pays: string;
current_pays: string;
date: number;
open_pays: string;
}
export interface OrdersData {
bids: OrdersDataItem[];
asks: OrdersDataItem[];
trading: OrdersDataItem[];
bids: OrdersDataItem[];
asks: OrdersDataItem[];
trading: OrdersDataItem[];
}
export interface ConversionRequest {
amount: string;
conversion_date: string;
id: number;
owner: string;
requestid: number;
amount: string;
conversion_date: string;
id: number;
owner: string;
requestid: number;
}
export interface SavingsWithdrawRequest {
id: number;
from: string;
to: string;
memo: string;
request_id: number;
amount: string;
complete: string;
}
id: number;
from: string;
to: string;
memo: string;
request_id: number;
amount: string;
complete: string;
}

View File

@ -4,209 +4,201 @@ import { Image } from 'react-native-image-crop-picker';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { toastNotification } from '../../redux/actions/uiAction';
import {
addFragment,
addImage,
deleteFragment,
deleteImage,
getFragments,
getImages,
updateFragment,
uploadImage,
addFragment,
addImage,
deleteFragment,
deleteImage,
getFragments,
getImages,
updateFragment,
uploadImage,
} from '../ecency/ecency';
import { MediaItem, Snippet } from '../ecency/ecency.types';
import { signImage } from '../hive/dhive';
import QUERIES from './queryKeys';
interface SnippetMutationVars {
id: string | null;
title: string;
body: string;
id: string | null;
title: string;
body: string;
}
interface MediaUploadVars {
media: Image;
addToUploads: boolean;
media: Image;
addToUploads: boolean;
}
/** GET QUERIES **/
export const useMediaQuery = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
return useQuery<MediaItem[]>([QUERIES.MEDIA.GET], getImages, {
initialData: [],
onError: () => {
dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' })));
},
});
const intl = useIntl();
const dispatch = useAppDispatch();
return useQuery<MediaItem[]>([QUERIES.MEDIA.GET], getImages, {
initialData: [],
onError: () => {
dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' })));
},
});
};
export const useSnippetsQuery = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
return useQuery<Snippet[]>([QUERIES.SNIPPETS.GET], getFragments, {
initialData: [],
onError: () => {
dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' })));
},
});
const intl = useIntl();
const dispatch = useAppDispatch();
return useQuery<Snippet[]>([QUERIES.SNIPPETS.GET], getFragments, {
initialData: [],
onError: () => {
dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' })));
},
});
};
/** ADD UPDATE MUTATIONS **/
export const useAddToUploadsMutation = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const queryClient = useQueryClient();
const intl = useIntl();
const dispatch = useAppDispatch();
const queryClient = useQueryClient();
return useMutation<any[], Error, string>(addImage, {
retry: 3,
onSuccess: (data) => {
queryClient.setQueryData([QUERIES.MEDIA.GET], data);
},
onError: (error) => {
if (error.toString().includes('code 409')) {
//means image ware already preset, refresh to get updated order
queryClient.invalidateQueries([QUERIES.MEDIA.GET]);
} else {
dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' })));
}
}
})
}
export const useMediaUploadMutation = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const addToUploadsMutation = useAddToUploadsMutation();
const currentAccount = useAppSelector(state => state.account.currentAccount);
const pinCode = useAppSelector(state => state.application.pin);
return useMutation<Image, undefined, MediaUploadVars>(
async ({ media }) => {
console.log('uploading media', media);
let sign = await signImage(media, currentAccount, pinCode);
return await uploadImage(media, currentAccount.name, sign);
},
{
retry: 3,
onSuccess: (response, { addToUploads }) => {
if (addToUploads && response && response.url) {
console.log('adding image to gallery', response.url);
addToUploadsMutation.mutate(response.url);
}
},
onError: () => {
dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' })));
},
},
);
}
export const useSnippetsMutation = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const queryClient = useQueryClient();
return useMutation<Snippet[], undefined, SnippetMutationVars>(
async (vars) => {
console.log('going to add/update snippet', vars);
if (vars.id) {
const response = await updateFragment(vars.id, vars.title, vars.body);
return response;
} else {
const response = await addFragment(vars.title, vars.body);
return response;
}
},
{
onMutate: (vars) => {
console.log('mutate snippets for add/update', vars);
const _newItem = {
id: vars.id,
title: vars.title,
body: vars.body,
created: new Date().toDateString(),
modified: new Date().toDateString(),
} as Snippet;
const data = queryClient.getQueryData<Snippet[]>([QUERIES.SNIPPETS.GET]);
let _newData: Snippet[] = data ? [...data] : [];
if (vars.id) {
const snipIndex = _newData.findIndex((item) => vars.id === item.id);
_newData[snipIndex] = _newItem;
} else {
_newData = [_newItem, ..._newData];
}
queryClient.setQueryData([QUERIES.SNIPPETS.GET], _newData);
},
onSuccess: (data) => {
console.log('added/updated snippet', data);
queryClient.invalidateQueries([QUERIES.SNIPPETS.GET]);
},
onError: () => {
dispatch(toastNotification(intl.formatMessage({ id: 'snippets.message_failed' })));
},
},
);
return useMutation<any[], Error, string>(addImage, {
retry: 3,
onSuccess: (data) => {
queryClient.setQueryData([QUERIES.MEDIA.GET], data);
},
onError: (error) => {
if (error.toString().includes('code 409')) {
//means image ware already preset, refresh to get updated order
queryClient.invalidateQueries([QUERIES.MEDIA.GET]);
} else {
dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' })));
}
},
});
};
export const useMediaUploadMutation = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const addToUploadsMutation = useAddToUploadsMutation();
const currentAccount = useAppSelector((state) => state.account.currentAccount);
const pinCode = useAppSelector((state) => state.application.pin);
return useMutation<Image, undefined, MediaUploadVars>(
async ({ media }) => {
console.log('uploading media', media);
let sign = await signImage(media, currentAccount, pinCode);
return await uploadImage(media, currentAccount.name, sign);
},
{
retry: 3,
onSuccess: (response, { addToUploads }) => {
if (addToUploads && response && response.url) {
console.log('adding image to gallery', response.url);
addToUploadsMutation.mutate(response.url);
}
},
onError: () => {
dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' })));
},
},
);
};
export const useSnippetsMutation = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const queryClient = useQueryClient();
return useMutation<Snippet[], undefined, SnippetMutationVars>(
async (vars) => {
console.log('going to add/update snippet', vars);
if (vars.id) {
const response = await updateFragment(vars.id, vars.title, vars.body);
return response;
} else {
const response = await addFragment(vars.title, vars.body);
return response;
}
},
{
onMutate: (vars) => {
console.log('mutate snippets for add/update', vars);
const _newItem = {
id: vars.id,
title: vars.title,
body: vars.body,
created: new Date().toDateString(),
modified: new Date().toDateString(),
} as Snippet;
const data = queryClient.getQueryData<Snippet[]>([QUERIES.SNIPPETS.GET]);
let _newData: Snippet[] = data ? [...data] : [];
if (vars.id) {
const snipIndex = _newData.findIndex((item) => vars.id === item.id);
_newData[snipIndex] = _newItem;
} else {
_newData = [_newItem, ..._newData];
}
queryClient.setQueryData([QUERIES.SNIPPETS.GET], _newData);
},
onSuccess: (data) => {
console.log('added/updated snippet', data);
queryClient.invalidateQueries([QUERIES.SNIPPETS.GET]);
},
onError: () => {
dispatch(toastNotification(intl.formatMessage({ id: 'snippets.message_failed' })));
},
},
);
};
/** DELETE MUTATIONS **/
export const useMediaDeleteMutation = () => {
const queryClient = useQueryClient();
const dispatch = useAppDispatch();
const intl = useIntl();
return useMutation<string[], undefined, string[]>(async (deleteIds) => {
for (const i in deleteIds) {
await deleteImage(deleteIds[i]);
const queryClient = useQueryClient();
const dispatch = useAppDispatch();
const intl = useIntl();
return useMutation<string[], undefined, string[]>(
async (deleteIds) => {
for (const i in deleteIds) {
await deleteImage(deleteIds[i]);
}
return deleteIds;
},
{
retry: 3,
onSuccess: (deleteIds) => {
console.log('Success media deletion delete', deleteIds);
const data: MediaItem[] | undefined = queryClient.getQueryData([QUERIES.MEDIA.GET]);
if (data) {
const _newData = data.filter((item) => !deleteIds.includes(item._id));
queryClient.setQueryData([QUERIES.MEDIA.GET], _newData);
}
return deleteIds;
}, {
retry: 3,
onSuccess: (deleteIds) => {
console.log('Success media deletion delete', deleteIds);
const data: MediaItem[] | undefined = queryClient.getQueryData([QUERIES.MEDIA.GET]);
if (data) {
const _newData = data.filter((item) => (!deleteIds.includes(item._id)))
queryClient.setQueryData([QUERIES.MEDIA.GET], _newData);
}
},
onError: () => {
dispatch(toastNotification(intl.formatMessage({ id: 'uploads_modal.delete_failed' })));
queryClient.invalidateQueries([QUERIES.MEDIA.GET]);
},
});
},
onError: () => {
dispatch(toastNotification(intl.formatMessage({ id: 'uploads_modal.delete_failed' })));
queryClient.invalidateQueries([QUERIES.MEDIA.GET]);
},
},
);
};
export const useSnippetDeleteMutation = () => {
const queryClient = useQueryClient();
const dispatch = useAppDispatch();
const intl = useIntl();
return useMutation<Snippet[], undefined, string>(deleteFragment, {
retry: 3,
onSuccess: (data) => {
console.log('Success scheduled post delete', data);
queryClient.setQueryData([QUERIES.SNIPPETS.GET], data);
},
onError: () => {
dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' })));
},
});
const queryClient = useQueryClient();
const dispatch = useAppDispatch();
const intl = useIntl();
return useMutation<Snippet[], undefined, string>(deleteFragment, {
retry: 3,
onSuccess: (data) => {
console.log('Success scheduled post delete', data);
queryClient.setQueryData([QUERIES.SNIPPETS.GET], data);
},
onError: () => {
dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' })));
},
});
};

View File

@ -26,4 +26,4 @@ export const initQueryClient = () => {
export * from './notificationQueries';
export * from './draftQueries';
export * from './editorQueries';
export * from './pointQueries'
export * from './pointQueries';

View File

@ -1,77 +1,72 @@
import { useMutation } from "@tanstack/react-query"
import { useMutation } from '@tanstack/react-query';
import { useAppSelector, useAppDispatch } from '../../hooks';
import { deletePointActivityCache, updatePointActivityCache } from "../../redux/actions/cacheActions";
import { generateRndStr } from "../../utils/editor";
import { PointActivity, PointActivityIds } from "../ecency/ecency.types";
import { userActivity } from "../ecency/ePoint"
import {
deletePointActivityCache,
updatePointActivityCache,
} from '../../redux/actions/cacheActions';
import { generateRndStr } from '../../utils/editor';
import { PointActivity, PointActivityIds } from '../ecency/ecency.types';
import { userActivity } from '../ecency/ePoint';
interface UserActivityMutationVars {
pointsTy: PointActivityIds;
blockNum?: string|number;
transactionId?: string;
cacheId?: string
pointsTy: PointActivityIds;
blockNum?: string | number;
transactionId?: string;
cacheId?: string;
}
export const useUserActivityMutation = () => {
const dispatch = useAppDispatch();
const currentAccount = useAppSelector(state => state.account.currentAccount);
const pointActivitiesCache:Map<string, PointActivity> = useAppSelector(state => state.cache.pointActivities);
const dispatch = useAppDispatch();
const currentAccount = useAppSelector((state) => state.account.currentAccount);
const pointActivitiesCache: Map<string, PointActivity> = useAppSelector(
(state) => state.cache.pointActivities,
);
const _mutationFn = async ({ pointsTy, blockNum, transactionId }: UserActivityMutationVars) => {
await userActivity(pointsTy, transactionId, blockNum);
return true;
};
const _mutationFn = async ({ pointsTy, blockNum, transactionId }: UserActivityMutationVars) => {
await userActivity(pointsTy, transactionId, blockNum)
return true;
}
const mutation = useMutation<boolean, Error, UserActivityMutationVars>(_mutationFn, {
retry: 2,
onSuccess: (data, vars) => {
console.log('successfully logged activity', data, vars);
//remove entry from redux
if (vars.cacheId) {
console.log('must remove from redux');
dispatch(deletePointActivityCache(vars.cacheId));
}
},
onError: (error, vars) => {
console.log('failed to log activity', error, vars);
//add entry in redux
if (!vars.cacheId && currentAccount) {
console.log('must add to from redux');
const cacheId = generateRndStr();
const { username } = currentAccount;
dispatch(updatePointActivityCache(cacheId, { ...vars, username }));
}
},
});
const mutation = useMutation<boolean, Error, UserActivityMutationVars>(_mutationFn, {
retry: 2,
onSuccess: (data, vars) => {
console.log("successfully logged activity", data, vars)
//remove entry from redux
if (vars.cacheId) {
console.log("must remove from redux")
dispatch(deletePointActivityCache(vars.cacheId))
}
},
onError: (error, vars) => {
console.log("failed to log activity", error, vars)
//add entry in redux
if (!vars.cacheId && currentAccount) {
console.log("must add to from redux")
const cacheId = generateRndStr();
const { username } = currentAccount;
dispatch(updatePointActivityCache(cacheId, { ...vars, username }))
}
}
})
const lazyMutatePendingActivities = () => {
setTimeout(() => {
//read pending activities from redux
if (currentAccount && pointActivitiesCache && pointActivitiesCache.size) {
Array.from(pointActivitiesCache).forEach(([id, activity]) => {
if (currentAccount.username === activity.username) {
mutation.mutate({
cacheId: id,
...activity,
});
}
});
}
}, 3000);
};
const lazyMutatePendingActivities = () => {
setTimeout(()=>{
//read pending activities from redux
if (currentAccount && pointActivitiesCache && pointActivitiesCache.size) {
Array.from(pointActivitiesCache).forEach(([id, activity]) => {
if (currentAccount.username === activity.username) {
mutation.mutate({
cacheId: id,
...activity,
})
}
})
}
}, 3000)
}
return {
...mutation,
lazyMutatePendingActivities,
}
}
return {
...mutation,
lazyMutatePendingActivities,
};
};

View File

@ -13,87 +13,99 @@ import {
DELETE_SUBSCRIBED_COMMUNITY_CACHE,
CLEAR_SUBSCRIBED_COMMUNITIES_CACHE,
DELETE_POINT_ACTIVITY_CACHE_ENTRY,
UPDATE_POINT_ACTIVITY_CACHE
UPDATE_POINT_ACTIVITY_CACHE,
} from '../constants/constants';
import { Comment, CommentCacheStatus, Draft, SubscribedCommunity, Vote } from '../reducers/cacheReducer';
import {
Comment,
CommentCacheStatus,
Draft,
SubscribedCommunity,
Vote,
} from '../reducers/cacheReducer';
export const updateVoteCache = (postPath: string, vote: Vote) => ({
payload: {
postPath,
vote
vote,
},
type: UPDATE_VOTE_CACHE
})
type: UPDATE_VOTE_CACHE,
});
interface CommentCacheOptions {
isUpdate?: boolean;
parentTags?: Array<string>;
}
export const updateCommentCache = (commentPath: string, comment: Comment, options: CommentCacheOptions = { isUpdate: false }) => {
console.log("body received:", comment.markdownBody);
export const updateCommentCache = (
commentPath: string,
comment: Comment,
options: CommentCacheOptions = { isUpdate: false },
) => {
console.log('body received:', comment.markdownBody);
const updated = new Date();
updated.setSeconds(updated.getSeconds() - 5); //make cache delayed by 5 seconds to avoid same updated stamp in post data
const updatedStamp = updated.toISOString().substring(0, 19); //server only return 19 character time string without timezone part
if (options.isUpdate && !comment.created) {
throw new Error("For comment update, created prop must be provided from original comment data to update local cache");
throw new Error(
'For comment update, created prop must be provided from original comment data to update local cache',
);
}
if (!options.parentTags && !comment.json_metadata) {
throw new Error("either of json_metadata in comment data or parentTags in options must be provided");
throw new Error(
'either of json_metadata in comment data or parentTags in options must be provided',
);
}
comment.created = comment.created || updatedStamp; //created will be set only once for new comment;
comment.created = comment.created || updatedStamp; //created will be set only once for new comment;
comment.updated = comment.updated || updatedStamp;
comment.expiresAt = comment.expiresAt || updated.getTime() + 6000000;//600000;
comment.expiresAt = comment.expiresAt || updated.getTime() + 6000000; //600000;
comment.active_votes = comment.active_votes || [];
comment.net_rshares = comment.net_rshares || 0;
comment.author_reputation = comment.author_reputation || 25;
comment.total_payout = comment.total_payout || 0;
comment.json_metadata = comment.json_metadata || makeJsonMetadataReply(options.parentTags)
comment.json_metadata = comment.json_metadata || makeJsonMetadataReply(options.parentTags);
comment.isDeletable = comment.isDeletable || true;
comment.status = comment.status || CommentCacheStatus.PENDING;
comment.body = renderPostBody({
author: comment.author,
permlink: comment.permlink,
last_update: comment.updated,
body: comment.markdownBody,
}, true, Platform.OS === 'android');
comment.body = renderPostBody(
{
author: comment.author,
permlink: comment.permlink,
last_update: comment.updated,
body: comment.markdownBody,
},
true,
Platform.OS === 'android',
);
return ({
return {
payload: {
commentPath,
comment
comment,
},
type: UPDATE_COMMENT_CACHE
})
}
type: UPDATE_COMMENT_CACHE,
};
};
export const deleteCommentCacheEntry = (commentPath: string) => ({
payload: commentPath,
type: DELETE_COMMENT_CACHE_ENTRY
})
type: DELETE_COMMENT_CACHE_ENTRY,
});
export const updateDraftCache = (id: string, draft: Draft) => ({
payload: {
id,
draft
draft,
},
type: UPDATE_DRAFT_CACHE
})
type: UPDATE_DRAFT_CACHE,
});
export const deleteDraftCacheEntry = (id: string) => ({
payload: id,
type: DELETE_DRAFT_CACHE_ENTRY
})
type: DELETE_DRAFT_CACHE_ENTRY,
});
export const updateSubscribedCommunitiesCache = (data: any) => {
const path = data.communityId;
@ -102,43 +114,42 @@ export const updateSubscribedCommunitiesCache = (data: any) => {
const userRole = data.userRole ? data.userRole : '';
const userLabel = data.userLabel ? data.userLabel : '';
const subscribedCommunity:SubscribedCommunity = {
data : [data.communityId, communityTitle, userRole, userLabel, !data.isSubscribed],
expiresAt : created.getTime() + 86400000,
const subscribedCommunity: SubscribedCommunity = {
data: [data.communityId, communityTitle, userRole, userLabel, !data.isSubscribed],
expiresAt: created.getTime() + 86400000,
};
return ({
return {
payload: {
path,
subscribedCommunity
subscribedCommunity,
},
type: UPDATE_SUBSCRIBED_COMMUNITY_CACHE
})
}
type: UPDATE_SUBSCRIBED_COMMUNITY_CACHE,
};
};
export const deleteSubscribedCommunityCacheEntry = (path: string) => ({
payload: path,
type: DELETE_SUBSCRIBED_COMMUNITY_CACHE
})
type: DELETE_SUBSCRIBED_COMMUNITY_CACHE,
});
export const clearSubscribedCommunitiesCache = () => ({
type: CLEAR_SUBSCRIBED_COMMUNITIES_CACHE
})
type: CLEAR_SUBSCRIBED_COMMUNITIES_CACHE,
});
export const updatePointActivityCache = (id:string, pointActivity: PointActivity) => ({
export const updatePointActivityCache = (id: string, pointActivity: PointActivity) => ({
payload: {
id,
pointActivity
pointActivity,
},
type: UPDATE_POINT_ACTIVITY_CACHE
})
type: UPDATE_POINT_ACTIVITY_CACHE,
});
export const deletePointActivityCache = (id: string) => ({
payload: id,
type: DELETE_POINT_ACTIVITY_CACHE_ENTRY
})
type: DELETE_POINT_ACTIVITY_CACHE_ENTRY,
});
export const purgeExpiredCache = () => ({
type: PURGE_EXPIRED_CACHE
})
type: PURGE_EXPIRED_CACHE,
});

View File

@ -1,82 +1,86 @@
import { getLatestQuotes } from '../../providers/ecency/ecency';
import { fetchCoinsData } from '../../utils/wallet';
import { SET_SELECTED_COINS, SET_PRICE_HISTORY, SET_COINS_DATA, SET_COIN_ACTIVITIES, SET_COIN_QUOTES, RESET_WALLET_DATA } from '../constants/constants';
import {
SET_SELECTED_COINS,
SET_PRICE_HISTORY,
SET_COINS_DATA,
SET_COIN_ACTIVITIES,
SET_COIN_QUOTES,
RESET_WALLET_DATA,
} from '../constants/constants';
import { CoinActivitiesCollection, CoinBase, CoinData } from '../reducers/walletReducer';
import { AppDispatch, RootState } from '../store/store';
export const setSelectedCoins = (coins: CoinBase[]) => ({
payload: coins,
type: SET_SELECTED_COINS,
payload: coins,
type: SET_SELECTED_COINS,
});
export const setCoinsData = (data: { [key: string]: CoinData }, vsCurrency: string, username: string) => ({
payload: {
data,
vsCurrency,
username,
},
type: SET_COINS_DATA
})
export const setCoinsData = (
data: { [key: string]: CoinData },
vsCurrency: string,
username: string,
) => ({
payload: {
data,
vsCurrency,
username,
},
type: SET_COINS_DATA,
});
export const setPriceHistory = (coinId: string, vsCurrency: string, data: number[]) => ({
payload: {
id: coinId,
vsCurrency,
data
},
type: SET_PRICE_HISTORY
})
payload: {
id: coinId,
vsCurrency,
data,
},
type: SET_PRICE_HISTORY,
});
export const setCoinActivities = (coinId: string, data: CoinActivitiesCollection) => ({
payload: {
id: coinId,
data,
},
type: SET_COIN_ACTIVITIES
})
payload: {
id: coinId,
data,
},
type: SET_COIN_ACTIVITIES,
});
export const resetWalletData = () => ({
type: RESET_WALLET_DATA
})
type: RESET_WALLET_DATA,
});
export const fetchCoinQuotes = () => (dispatch, getState) => {
const currency = getState().application.currency;
console.log("fetching quotes for currency", currency)
getLatestQuotes(currency.currencyRate)
.then((quotes) => {
console.log("Fetched quotes", quotes)
dispatch({
type: SET_COIN_QUOTES,
payload: { ...quotes },
})
})
}
const currency = getState().application.currency;
console.log('fetching quotes for currency', currency);
getLatestQuotes(currency.currencyRate).then((quotes) => {
console.log('Fetched quotes', quotes);
dispatch({
type: SET_COIN_QUOTES,
payload: { ...quotes },
});
});
};
export const fetchAndSetCoinsData = (refresh: boolean = false) => async (
dispatch: AppDispatch,
getState: RootState,
) => {
const coins = getState().wallet.selectedCoins;
const quotes = getState().wallet.quotes;
const currentAccount = getState().account.currentAccount;
const currency = getState().application.currency;
const globalProps = getState().account.globalProps;
const coinsData = await fetchCoinsData({
coins,
currentAccount,
vsCurrency: currency.currency,
currencyRate: currency.currencyRate,
globalProps,
quotes,
refresh,
});
export const fetchAndSetCoinsData = (refresh: boolean = false) => async (dispatch: AppDispatch, getState: RootState) => {
const coins = getState().wallet.selectedCoins;
const quotes = getState().wallet.quotes;
const currentAccount = getState().account.currentAccount;
const currency = getState().application.currency;
const globalProps = getState().account.globalProps;
const coinsData = await fetchCoinsData({
coins,
currentAccount,
vsCurrency: currency.currency,
currencyRate: currency.currencyRate,
globalProps,
quotes,
refresh
})
return dispatch(setCoinsData(
coinsData,
currency.currency,
currentAccount.username
))
}
return dispatch(setCoinsData(coinsData, currency.currency, currentAccount.username));
};

View File

@ -1,6 +1,5 @@
export const statusMessage = {
PENDING : 'PENDING',
SUCCESS : 'SUCCESS',
FAIL : 'FAIL',
PENDING: 'PENDING',
SUCCESS: 'SUCCESS',
FAIL: 'FAIL',
};

View File

@ -12,25 +12,23 @@ import {
SET_GLOBAL_PROPS,
} from '../constants/constants';
export interface GlobalProps {
hivePerMVests:number;
base:number;
quote:number;
fundRecentClaims:number;
fundRewardBalance:number;
hbdPrintRate:number;
hivePerMVests: number;
base: number;
quote: number;
fundRecentClaims: number;
fundRewardBalance: number;
hbdPrintRate: number;
}
interface AccountState {
isFetching:boolean;
isFetching: boolean;
currentAccount: any;
otherAccounts: any[];
hasError: boolean;
errorMessage: string;
isLogingOut: boolean;
globalProps:GlobalProps|null;
globalProps: GlobalProps | null;
}
const initialState: AccountState = {
@ -40,7 +38,7 @@ const initialState: AccountState = {
hasError: false,
errorMessage: null,
isLogingOut: false,
globalProps: null
globalProps: null,
};
export default function (state = initialState, action) {

View File

@ -1,235 +1,231 @@
import { PointActivity } from "../../providers/ecency/ecency.types";
import { PointActivity } from '../../providers/ecency/ecency.types';
import {
PURGE_EXPIRED_CACHE,
UPDATE_VOTE_CACHE,
UPDATE_COMMENT_CACHE,
DELETE_COMMENT_CACHE_ENTRY,
DELETE_DRAFT_CACHE_ENTRY,
UPDATE_DRAFT_CACHE,
UPDATE_SUBSCRIBED_COMMUNITY_CACHE,
DELETE_SUBSCRIBED_COMMUNITY_CACHE,
CLEAR_SUBSCRIBED_COMMUNITIES_CACHE,
UPDATE_POINT_ACTIVITY_CACHE,
DELETE_POINT_ACTIVITY_CACHE_ENTRY
} from "../constants/constants";
PURGE_EXPIRED_CACHE,
UPDATE_VOTE_CACHE,
UPDATE_COMMENT_CACHE,
DELETE_COMMENT_CACHE_ENTRY,
DELETE_DRAFT_CACHE_ENTRY,
UPDATE_DRAFT_CACHE,
UPDATE_SUBSCRIBED_COMMUNITY_CACHE,
DELETE_SUBSCRIBED_COMMUNITY_CACHE,
CLEAR_SUBSCRIBED_COMMUNITIES_CACHE,
UPDATE_POINT_ACTIVITY_CACHE,
DELETE_POINT_ACTIVITY_CACHE_ENTRY,
} from '../constants/constants';
export enum CommentCacheStatus {
PENDING = 'PENDING',
POSTPONED = 'PUBLISHED',
DELETED = 'DELETED',
PENDING = 'PENDING',
POSTPONED = 'PUBLISHED',
DELETED = 'DELETED',
}
export interface Vote {
amount: number;
isDownvote: boolean;
incrementStep: number;
votedAt: number;
expiresAt: number;
amount: number;
isDownvote: boolean;
incrementStep: number;
votedAt: number;
expiresAt: number;
}
export interface Comment {
author: string,
permlink: string,
parent_author: string,
parent_permlink: string,
body?: string,
markdownBody: string,
author_reputation?: number,
total_payout?: number,
net_rshares?: number,
active_votes?: Array<{ rshares: number, voter: string }>,
json_metadata?: any,
isDeletable?: boolean,
created?: string, //handle created and updated separatly
updated?: string,
expiresAt?: number,
status: CommentCacheStatus
author: string;
permlink: string;
parent_author: string;
parent_permlink: string;
body?: string;
markdownBody: string;
author_reputation?: number;
total_payout?: number;
net_rshares?: number;
active_votes?: Array<{ rshares: number; voter: string }>;
json_metadata?: any;
isDeletable?: boolean;
created?: string; //handle created and updated separatly
updated?: string;
expiresAt?: number;
status: CommentCacheStatus;
}
export interface Draft {
author: string,
body: string,
title?: string,
tags?: string,
meta?: any,
created?: number,
updated?: number,
expiresAt?: number;
author: string;
body: string;
title?: string;
tags?: string;
meta?: any;
created?: number;
updated?: number;
expiresAt?: number;
}
export interface SubscribedCommunity {
data: Array<any>,
expiresAt?: number;
data: Array<any>;
expiresAt?: number;
}
interface State {
votes: Map<string, Vote>
comments: Map<string, Comment> //TODO: handle comment array per post, if parent is same
drafts: Map<string, Draft>
subscribedCommunities: Map<string, SubscribedCommunity>
pointActivities: Map<string, PointActivity>
lastUpdate: {
postPath: string,
updatedAt: number,
type: 'vote' | 'comment' | 'draft',
}
votes: Map<string, Vote>;
comments: Map<string, Comment>; //TODO: handle comment array per post, if parent is same
drafts: Map<string, Draft>;
subscribedCommunities: Map<string, SubscribedCommunity>;
pointActivities: Map<string, PointActivity>;
lastUpdate: {
postPath: string;
updatedAt: number;
type: 'vote' | 'comment' | 'draft';
};
}
const initialState: State = {
votes: new Map(),
comments: new Map(),
drafts: new Map(),
subscribedCommunities: new Map(),
pointActivities: new Map(),
lastUpdate: null,
votes: new Map(),
comments: new Map(),
drafts: new Map(),
subscribedCommunities: new Map(),
pointActivities: new Map(),
lastUpdate: null,
};
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>();
}
state.votes.set(payload.postPath, payload.vote);
return {
...state, //spread operator in requried here, otherwise persist do not register change
lastUpdate: {
postPath: payload.postPath,
updatedAt: new Date().getTime(),
type: 'vote',
}
};
const { type, payload } = action;
switch (type) {
case UPDATE_VOTE_CACHE:
if (!state.votes) {
state.votes = new Map<string, Vote>();
}
state.votes.set(payload.postPath, payload.vote);
return {
...state, //spread operator in requried here, otherwise persist do not register change
lastUpdate: {
postPath: payload.postPath,
updatedAt: new Date().getTime(),
type: 'vote',
},
};
case UPDATE_COMMENT_CACHE:
if (!state.comments) {
state.comments = new Map<string, Comment>();
}
state.comments.set(payload.commentPath, payload.comment);
return {
...state, //spread operator in requried here, otherwise persist do not register change
lastUpdate: {
postPath: payload.commentPath,
updatedAt: new Date().getTime(),
type: 'comment'
}
};
case UPDATE_COMMENT_CACHE:
if (!state.comments) {
state.comments = new Map<string, Comment>();
}
state.comments.set(payload.commentPath, payload.comment);
return {
...state, //spread operator in requried here, otherwise persist do not register change
lastUpdate: {
postPath: payload.commentPath,
updatedAt: new Date().getTime(),
type: 'comment',
},
};
case DELETE_COMMENT_CACHE_ENTRY:
if (state.comments && state.comments.has(payload)) {
state.comments.delete(payload);
}
return { ...state }
case DELETE_COMMENT_CACHE_ENTRY:
if (state.comments && state.comments.has(payload)) {
state.comments.delete(payload);
}
return { ...state };
case UPDATE_DRAFT_CACHE:
if (!state.drafts) {
state.drafts = new Map<string, Draft>();
}
case UPDATE_DRAFT_CACHE:
if (!state.drafts) {
state.drafts = new Map<string, Draft>();
}
const curTime = new Date().getTime();
const curDraft = state.drafts.get(payload.id);
const payloadDraft = payload.draft;
const curTime = new Date().getTime();
const curDraft = state.drafts.get(payload.id);
const payloadDraft = payload.draft;
payloadDraft.created = curDraft ? curDraft.created : curTime;
payloadDraft.updated = curTime;
payloadDraft.expiresAt = curTime + 604800000 // 7 days ms
payloadDraft.created = curDraft ? curDraft.created : curTime;
payloadDraft.updated = curTime;
payloadDraft.expiresAt = curTime + 604800000; // 7 days ms
state.drafts.set(payload.id, payloadDraft);
return {
...state, //spread operator in requried here, otherwise persist do not register change
lastUpdate: {
postPath: payload.id,
updatedAt: new Date().getTime(),
type: 'draft',
},
};
state.drafts.set(payload.id, payloadDraft);
return {
...state, //spread operator in requried here, otherwise persist do not register change
lastUpdate: {
postPath: payload.id,
updatedAt: new Date().getTime(),
type: 'draft',
},
};
case DELETE_DRAFT_CACHE_ENTRY:
if (state.drafts && state.drafts.has(payload)) {
state.drafts.delete(payload);
}
return { ...state }
case DELETE_DRAFT_CACHE_ENTRY:
if (state.drafts && state.drafts.has(payload)) {
state.drafts.delete(payload);
}
return { ...state };
case UPDATE_SUBSCRIBED_COMMUNITY_CACHE:
if (!state.subscribedCommunities) {
state.subscribedCommunities = new Map<string, SubscribedCommunity>();
}
const subscribedCommunities = new Map(state.subscribedCommunities);
subscribedCommunities.set(payload.path, payload.subscribedCommunity);
return {
...state, //spread operator in requried here, otherwise persist do not register change
subscribedCommunities: subscribedCommunities
};
case UPDATE_SUBSCRIBED_COMMUNITY_CACHE:
if (!state.subscribedCommunities) {
state.subscribedCommunities = new Map<string, SubscribedCommunity>();
}
const subscribedCommunities = new Map(state.subscribedCommunities);
subscribedCommunities.set(payload.path, payload.subscribedCommunity);
return {
...state, //spread operator in requried here, otherwise persist do not register change
subscribedCommunities: subscribedCommunities,
};
case DELETE_SUBSCRIBED_COMMUNITY_CACHE:
if (state.subscribedCommunities && state.subscribedCommunities.has(payload)) {
state.subscribedCommunities.delete(payload);
}
return { ...state }
case DELETE_SUBSCRIBED_COMMUNITY_CACHE:
if (state.subscribedCommunities && state.subscribedCommunities.has(payload)) {
state.subscribedCommunities.delete(payload);
}
return { ...state };
case CLEAR_SUBSCRIBED_COMMUNITIES_CACHE:
state.subscribedCommunities = new Map<string, SubscribedCommunity>();
case CLEAR_SUBSCRIBED_COMMUNITIES_CACHE:
state.subscribedCommunities = new Map<string, SubscribedCommunity>();
return { ...state }
return { ...state };
case UPDATE_POINT_ACTIVITY_CACHE:
if (!state.pointActivities) {
state.pointActivities = new Map<string, PointActivity>();
}
state.pointActivities.set(payload.id, payload.pointActivity);
return {
...state, //spread operator in requried here, otherwise persist do not register change
};
case UPDATE_POINT_ACTIVITY_CACHE:
if (!state.pointActivities) {
state.pointActivities = new Map<string, PointActivity>();
}
state.pointActivities.set(payload.id, payload.pointActivity);
return {
...state, //spread operator in requried here, otherwise persist do not register change
};
case DELETE_POINT_ACTIVITY_CACHE_ENTRY:
if (state.pointActivities && state.pointActivities.has(payload)) {
state.pointActivities.delete(payload);
}
return { ...state }
case DELETE_POINT_ACTIVITY_CACHE_ENTRY:
if (state.pointActivities && state.pointActivities.has(payload)) {
state.pointActivities.delete(payload);
}
return { ...state };
case PURGE_EXPIRED_CACHE:
const currentTime = new Date().getTime();
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.votes && state.votes.size) {
Array.from(state.votes).forEach((entry) => {
if (entry[1].expiresAt < currentTime) {
state.votes.delete(entry[0]);
}
});
}
if (state.comments && state.comments.size) {
Array.from(state.comments).forEach((entry) => {
if (entry[1].expiresAt < currentTime) {
state.comments.delete(entry[0]);
}
})
}
if (state.comments && state.comments.size) {
Array.from(state.comments).forEach((entry) => {
if (entry[1].expiresAt < currentTime) {
state.comments.delete(entry[0]);
}
});
}
if (state.drafts && state.drafts.size) {
Array.from(state.drafts).forEach((entry) => {
if (entry[1].expiresAt < currentTime || !entry[1].body) {
state.drafts.delete(entry[0]);
}
})
if (state.drafts && state.drafts.size) {
Array.from(state.drafts).forEach((entry) => {
if (entry[1].expiresAt < currentTime || !entry[1].body) {
state.drafts.delete(entry[0]);
}
});
}
if (state.subscribedCommunities && state.subscribedCommunities.size) {
Array.from(state.subscribedCommunities).forEach((entry) => {
if (entry[1].expiresAt < currentTime) {
state.subscribedCommunities.delete(entry[0]);
}
});
}
}
if (state.subscribedCommunities && state.subscribedCommunities.size) {
Array.from(state.subscribedCommunities).forEach((entry) => {
if (entry[1].expiresAt < currentTime) {
state.subscribedCommunities.delete(entry[0]);
}
})
}
return {
...state
}
default:
return state;
}
return {
...state,
};
default:
return state;
}
}

View File

@ -1,37 +1,36 @@
import { REMOVE_BENEFICIARIES, SET_BENEFICIARIES } from "../constants/constants";
import { REMOVE_BENEFICIARIES, SET_BENEFICIARIES } from '../constants/constants';
export interface Beneficiary {
account:string,
weight:number,
isValid?:boolean,
autoPowerUp?: boolean,
account: string;
weight: number;
isValid?: boolean;
autoPowerUp?: boolean;
}
interface State {
beneficiariesMap:{
[key: string]: Beneficiary[]
}
beneficiariesMap: {
[key: string]: Beneficiary[];
};
}
const initialState:State = {
beneficiariesMap:{}
};
export default function (state = initialState, action) {
const {type, payload} = action;
switch (type) {
case SET_BENEFICIARIES:
state.beneficiariesMap[payload.draftId] = payload.benficiaries;
return {
...state //spread operator in requried here, otherwise persist do not register change
};
case REMOVE_BENEFICIARIES:
delete state.beneficiariesMap[payload.draftId]
return {
...state //spread operator in requried here, otherwise persist do not register change
};
default:
return state;
}
}
const initialState: State = {
beneficiariesMap: {},
};
export default function (state = initialState, action) {
const { type, payload } = action;
switch (type) {
case SET_BENEFICIARIES:
state.beneficiariesMap[payload.draftId] = payload.benficiaries;
return {
...state, //spread operator in requried here, otherwise persist do not register change
};
case REMOVE_BENEFICIARIES:
delete state.beneficiariesMap[payload.draftId];
return {
...state, //spread operator in requried here, otherwise persist do not register change
};
default:
return state;
}
}

View File

@ -1,140 +1,144 @@
import DEFAULT_COINS from "../../constants/defaultCoins";
import { SET_PRICE_HISTORY, SET_SELECTED_COINS, SET_COINS_DATA, SET_COIN_ACTIVITIES, SET_COIN_QUOTES, RESET_WALLET_DATA } from "../constants/constants";
import DEFAULT_COINS from '../../constants/defaultCoins';
import {
SET_PRICE_HISTORY,
SET_SELECTED_COINS,
SET_COINS_DATA,
SET_COIN_ACTIVITIES,
SET_COIN_QUOTES,
RESET_WALLET_DATA,
} from '../constants/constants';
export interface DataPair {
value:string|number;
dataKey:string;
isClickable?:boolean;
value: string | number;
dataKey: string;
isClickable?: boolean;
}
export interface CoinBase {
id:string,
symbol:string,
notCrypto:boolean,
id: string;
symbol: string;
notCrypto: boolean;
}
export interface CoinData {
currentPrice:number;
balance:number;
savings?:number;
unclaimedBalance:string,
estimateValue?:number;
vsCurrency:string;
actions:string[];
extraDataPairs?:DataPair[];
currentPrice: number;
balance: number;
savings?: number;
unclaimedBalance: string;
estimateValue?: number;
vsCurrency: string;
actions: string[];
extraDataPairs?: DataPair[];
}
export interface PriceHistory {
expiresAt:number;
vsCurrency:string;
data:number[];
expiresAt: number;
vsCurrency: string;
data: number[];
}
export interface CoinActivity {
trxIndex:number;
iconType: string;
textKey: string;
created: string;
expires: string;
icon: string;
value:string;
details: string;
memo: string;
trxIndex: number;
iconType: string;
textKey: string;
created: string;
expires: string;
icon: string;
value: string;
details: string;
memo: string;
}
export interface QuoteItem {
lastUpdated:string;
percentChange:number;
price:number;
lastUpdated: string;
percentChange: number;
price: number;
}
export interface CoinActivitiesCollection {
completed:CoinActivity[],
pending:CoinActivity[],
completed: CoinActivity[];
pending: CoinActivity[];
}
interface State {
selectedCoins:CoinBase[];
coinsData:{
[key: string]: CoinData;
},
priceHistories:{
[key: string]: PriceHistory;
}
coinsActivities:{
[key: string]:CoinActivitiesCollection;
},
quotes:{
[key: string]: QuoteItem;
}
vsCurrency:string,
username:string,
updateTimestamp:number;
selectedCoins: CoinBase[];
coinsData: {
[key: string]: CoinData;
};
priceHistories: {
[key: string]: PriceHistory;
};
coinsActivities: {
[key: string]: CoinActivitiesCollection;
};
quotes: {
[key: string]: QuoteItem;
};
vsCurrency: string;
username: string;
updateTimestamp: number;
}
const initialState:State = {
selectedCoins:DEFAULT_COINS,
coinsData:{},
priceHistories:{},
coinsActivities:{},
quotes: null,
vsCurrency:'',
username:'',
updateTimestamp:0
const initialState: State = {
selectedCoins: DEFAULT_COINS,
coinsData: {},
priceHistories: {},
coinsActivities: {},
quotes: null,
vsCurrency: '',
username: '',
updateTimestamp: 0,
};
export default function (state = initialState, action) {
const {type, payload} = action;
switch (type) {
case RESET_WALLET_DATA:{
return {
...initialState,
selectedCoins:state.selectedCoins
}
const { type, payload } = action;
switch (type) {
case RESET_WALLET_DATA: {
return {
...initialState,
selectedCoins: state.selectedCoins,
};
}
case SET_SELECTED_COINS:{
return {
...state,
selectedCoin:payload
}
case SET_SELECTED_COINS: {
return {
...state,
selectedCoin: payload,
};
}
case SET_COINS_DATA:{
return {
...state,
coinsData:payload.data,
vsCurrency:payload.vsCurrency,
username:payload.username,
updateTimestamp:new Date().getTime()
}
case SET_COINS_DATA: {
return {
...state,
coinsData: payload.data,
vsCurrency: payload.vsCurrency,
username: payload.username,
updateTimestamp: new Date().getTime(),
};
}
case SET_PRICE_HISTORY:{
state.priceHistories[payload.id] = {
expiresAt:new Date().getTime() + ONE_HOUR_MS,
vsCurrency:payload.vsCurrency,
data:payload.data
};
return {
...state
}
case SET_PRICE_HISTORY: {
state.priceHistories[payload.id] = {
expiresAt: new Date().getTime() + ONE_HOUR_MS,
vsCurrency: payload.vsCurrency,
data: payload.data,
};
return {
...state,
};
}
case SET_COIN_ACTIVITIES:{
state.coinsActivities[payload.id] = payload.data
return {
...state
}
case SET_COIN_ACTIVITIES: {
state.coinsActivities[payload.id] = payload.data;
return {
...state,
};
}
case SET_COIN_QUOTES:{
return {
...state,
quotes:payload,
}
case SET_COIN_QUOTES: {
return {
...state,
quotes: payload,
};
}
default:
return state;
}
return state;
}
}
const ONE_HOUR_MS = 60 * 60 * 1000;
const ONE_HOUR_MS = 60 * 60 * 1000;

View File

@ -23,7 +23,7 @@ const transformCacheVoteMap = createTransform(
comments: new Map(outboundState.comments),
drafts: new Map(outboundState.drafts),
subscribedCommunities: new Map(outboundState.subscribedCommunities),
pointActivities: new Map(outboundState.pointActivities)
pointActivities: new Map(outboundState.pointActivities),
}),
{ whitelist: ['cache'] },
);

View File

@ -3,8 +3,6 @@ import { StatusBar, Platform, View, Alert, Text } from 'react-native';
import EStyleSheet from 'react-native-extended-stylesheet';
import { connect } from 'react-redux';
import { injectIntl } from 'react-intl';
import { AppNavigator } from '../../../navigation';
@ -34,7 +32,6 @@ import {
import darkTheme from '../../../themes/darkTheme';
import lightTheme from '../../../themes/lightTheme';
class ApplicationScreen extends Component {
constructor(props) {
super(props);
@ -97,7 +94,6 @@ class ApplicationScreen extends Component {
}
}
_renderStatusBar() {
const { isDarkTheme } = this.props;
const barStyle = isDarkTheme ? 'light-content' : 'dark-content';
@ -110,10 +106,9 @@ class ApplicationScreen extends Component {
<StatusBar barStyle={barStyle} backgroundColor={barColor} />
)}
</>
)
);
}
_renderAppNavigator() {
const { isConnected } = this.props;
return (
@ -121,17 +116,14 @@ class ApplicationScreen extends Component {
{!isConnected && <NoInternetConnection />}
<AppNavigator />
</Fragment>
)
);
}
_renderAppModals() {
const { toastNotification, foregroundNotificationData } = this.props;
const { isShowToastNotification } = this.state;
return (
<>
<ForegroundNotification remoteMessage={foregroundNotificationData} />
@ -148,11 +140,9 @@ class ApplicationScreen extends Component {
/>
)}
</>
)
);
}
render() {
return (
<View style={{ flex: 1 }}>

View File

@ -2,6 +2,7 @@ import { useEffect, useRef } from 'react';
import Orientation, { useDeviceOrientationChange } from 'react-native-orientation-locker';
import { isLandscape } from 'react-native-device-info';
import EStyleSheet from 'react-native-extended-stylesheet';
import { AppState } from 'react-native';
import { useAppDispatch, useAppSelector } from '../../../hooks';
import { setDeviceOrientation, setLockedOrientation } from '../../../redux/actions/uiAction';
import { orientations } from '../../../redux/constants/orientationsConstants';
@ -9,9 +10,6 @@ import isAndroidTablet from '../../../utils/isAndroidTablet';
import darkTheme from '../../../themes/darkTheme';
import lightTheme from '../../../themes/lightTheme';
import { useUserActivityMutation } from '../../../providers/queries';
import { AppState } from 'react-native';
export const useInitApplication = () => {
const dispatch = useAppDispatch();
@ -47,21 +45,15 @@ export const useInitApplication = () => {
return _cleanup;
}, []);
const _cleanup = () => {
AppState.removeEventListener('change', _handleAppStateChange);
}
};
const _handleAppStateChange = (nextAppState) => {
if (appState.current.match(/inactive|background/) && nextAppState === 'active') {
userActivityMutation.lazyMutatePendingActivities();
}
appState.current = nextAppState;
};
};

View File

@ -2,14 +2,14 @@ import { ViewStyle } from 'react-native';
import EStyleSheet from 'react-native-extended-stylesheet';
export default EStyleSheet.create({
userRibbonContainer: {
borderBottomWidth: EStyleSheet.hairlineWidth,
borderColor: '$darkGrayBackground',
marginBottom: 0, //without 0 margin, view will start overlapping UserRibbon
paddingBottom: 32
} as ViewStyle,
userRibbonContainer: {
borderBottomWidth: EStyleSheet.hairlineWidth,
borderColor: '$darkGrayBackground',
marginBottom: 0, //without 0 margin, view will start overlapping UserRibbon
paddingBottom: 32,
} as ViewStyle,
listContainer: {
paddingTop: 16
} as ViewStyle
listContainer: {
paddingTop: 16,
} as ViewStyle,
});

View File

@ -1,4 +1,4 @@
import React, { ComponentType, JSXElementConstructor, ReactElement } from 'react'
import React, { ComponentType, JSXElementConstructor, ReactElement } from 'react';
import { useIntl } from 'react-intl';
import { SectionList, Text, RefreshControl, ActivityIndicator } from 'react-native';
import { Transaction } from '../../../components';
@ -7,7 +7,7 @@ import { CoinActivity } from '../../../redux/reducers/walletReducer';
import styles from './children.styles';
interface ActivitiesListProps {
header: ComponentType<any> | ReactElement<any, string | JSXElementConstructor<any>>
header: ComponentType<any> | ReactElement<any, string | JSXElementConstructor<any>>;
pendingActivities: CoinActivity[];
completedActivities: CoinActivity[];
refreshing: boolean;
@ -16,7 +16,6 @@ interface ActivitiesListProps {
onRefresh: () => void;
}
const ActivitiesList = ({
header,
loading,
@ -24,31 +23,29 @@ const ActivitiesList = ({
completedActivities,
pendingActivities,
onEndReached,
onRefresh
onRefresh,
}: ActivitiesListProps) => {
const intl = useIntl();
const isDarkTheme = useAppSelector(state => state.ui.isDarkTheme);
const isDarkTheme = useAppSelector((state) => state.ui.isDarkTheme);
const _renderActivityItem = ({ item, index }) => {
return <Transaction item={item} index={index} />
}
return <Transaction item={item} index={index} />;
};
const sections = [];
if (pendingActivities && pendingActivities.length) {
sections.push({
title: intl.formatMessage({ id: 'wallet.pending_requests' }),
data: pendingActivities
})
data: pendingActivities,
});
}
sections.push({
title: intl.formatMessage({ id: 'wallet.activities' }),
data: completedActivities || []
})
data: completedActivities || [],
});
const _refreshControl = (
<RefreshControl
@ -59,8 +56,7 @@ const ActivitiesList = ({
titleColor="#fff"
colors={['#fff']}
/>
)
);
return (
<SectionList
@ -72,14 +68,16 @@ const ActivitiesList = ({
renderSectionHeader={({ section: { title } }) => (
<Text style={styles.textActivities}>{title}</Text>
)}
ListFooterComponent={loading && <ActivityIndicator style={styles.activitiesFooterIndicator} />}
ListFooterComponent={
loading && <ActivityIndicator style={styles.activitiesFooterIndicator} />
}
ListHeaderComponent={header}
refreshControl={_refreshControl}
onEndReached={()=>{onEndReached()}}
onEndReached={() => {
onEndReached();
}}
/>
)
}
export default ActivitiesList
);
};
export default ActivitiesList;

View File

@ -1,147 +1,143 @@
import { TextStyle, ViewStyle } from 'react-native';
import EStyleSheet from 'react-native-extended-stylesheet';
export const CHART_NEGATIVE_MARGIN = 12
export const CHART_NEGATIVE_MARGIN = 12;
export default EStyleSheet.create({
card: {
marginVertical:8,
borderRadius:12,
marginVertical: 8,
borderRadius: 12,
overflow: 'hidden',
backgroundColor: '$primaryLightBackground'
backgroundColor: '$primaryLightBackground',
} as ViewStyle,
basicsContainer:{
alignItems:'center',
padding:16
basicsContainer: {
alignItems: 'center',
padding: 16,
} as ViewStyle,
coinTitleContainer:{
flexDirection:'row',
marginTop:8
coinTitleContainer: {
flexDirection: 'row',
marginTop: 8,
} as ViewStyle,
textCoinTitle:{
color: '$primaryBlack',
textCoinTitle: {
color: '$primaryBlack',
fontSize: 34,
fontWeight:'700',
fontWeight: '700',
} as TextStyle,
textHeaderChange:{
textHeaderChange: {
color: '$primaryDarkText',
fontSize: 16,
marginBottom:32,
marginBottom: 32,
} as TextStyle,
textPositive:{
color: '$primaryGreen'
} as TextStyle,
textNegative:{
color: '$primaryRed'
} as TextStyle,
textBasicValue:{
color: '$primaryBlack',
fontWeight:'700',
fontSize: 28,
textPositive: {
color: '$primaryGreen',
} as TextStyle,
textBasicLabel:{
textNegative: {
color: '$primaryRed',
} as TextStyle,
textBasicValue: {
color: '$primaryBlack',
fontWeight: '700',
fontSize: 28,
} as TextStyle,
textBasicLabel: {
color: '$primaryDarkText',
fontSize: 14,
marginBottom:16,
marginBottom: 16,
} as TextStyle,
extraDataContainer:{
flexDirection:'row',
extraDataContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems:'center',
width:'100%',
alignItems: 'center',
width: '100%',
marginVertical: 2,
} as ViewStyle,
textExtraValue:{
textExtraValue: {
color: '$primaryDarkText',
fontWeight:'700',
fontWeight: '700',
fontSize: 18,
} as TextStyle,
textExtraLabel:{
textExtraLabel: {
color: '$primaryDarkText',
fontSize: 14,
} as TextStyle,
rangeContainer:{
flexDirection:'row',
alignItems:'center',
justifyContent:'space-between',
borderRadius:32,
rangeContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
borderRadius: 32,
} as ViewStyle,
rangeOptionWrapper:{
borderRadius:32,
paddingVertical:16,
paddingHorizontal:24
rangeOptionWrapper: {
borderRadius: 32,
paddingVertical: 16,
paddingHorizontal: 24,
} as ViewStyle,
textRange:{
fontSize:16,
textRange: {
fontSize: 16,
} as TextStyle,
chartContainer:{
chartContainer: {
height: 168,
marginTop: 16,
marginLeft:-CHART_NEGATIVE_MARGIN,
marginLeft: -CHART_NEGATIVE_MARGIN,
overflow: 'hidden',
} as ViewStyle,
list:{
flex:1,
list: {
flex: 1,
} as ViewStyle,
listContent: {
paddingBottom: 56,
paddingHorizontal: 16,
} as ViewStyle,
listContent:{
paddingBottom:56,
paddingHorizontal:16,
} as ViewStyle ,
//COIN ACTIONS STYLES
actionBtnContainer:{
flexGrow:1
actionBtnContainer: {
flexGrow: 1,
} as ViewStyle,
actionsContainer:{
flexDirection:'row',
flexWrap:'wrap'
actionsContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
} as ViewStyle,
actionContainer:{
paddingHorizontal:16,
marginVertical:8,
marginHorizontal:4,
backgroundColor:'$primaryLightBackground',
actionContainer: {
paddingHorizontal: 16,
marginVertical: 8,
marginHorizontal: 4,
backgroundColor: '$primaryLightBackground',
height: 40,
borderRadius:20,
justifyContent:'center',
alignItems:'center',
borderRadius: 20,
justifyContent: 'center',
alignItems: 'center',
} as ViewStyle,
actionText:{
color: '$primaryBlack'
actionText: {
color: '$primaryBlack',
} as TextStyle,
textActivities:{
color:'$primaryBlack',
fontWeight:'600',
fontSize:18,
paddingVertical:16,
backgroundColor:'$primaryBackgroundColor',
textActivities: {
color: '$primaryBlack',
fontWeight: '600',
fontSize: 18,
paddingVertical: 16,
backgroundColor: '$primaryBackgroundColor',
textAlign: 'left',
} as TextStyle,
activitiesFooterIndicator:{
marginVertical: 16
activitiesFooterIndicator: {
marginVertical: 16,
} as ViewStyle,
delegationsModal:{
delegationsModal: {
flex: 1,
backgroundColor: '$primaryBackgroundColor',
margin: 0,
paddingTop: 32,
paddingBottom: 16,
},
textUnderline:{
textDecorationLine:'underline',
textDecorationColor:'$primaryDarkText'
}
textUnderline: {
textDecorationLine: 'underline',
textDecorationColor: '$primaryDarkText',
},
});

View File

@ -1,38 +1,36 @@
import React, { Fragment } from 'react'
import { useIntl } from 'react-intl'
import { View, Text } from 'react-native'
import { TouchableOpacity } from 'react-native-gesture-handler'
import { withNavigation } from '@react-navigation/compat'
import styles from './children.styles'
import React, { Fragment } from 'react';
import { useIntl } from 'react-intl';
import { View, Text } from 'react-native';
import { TouchableOpacity } from 'react-native-gesture-handler';
import { withNavigation } from '@react-navigation/compat';
import styles from './children.styles';
interface CoinActionsProps {
actions: string[];
onActionPress: (action: string) => void;
actions: string[];
onActionPress: (action: string) => void;
}
export const CoinActions = withNavigation(({ actions, onActionPress }: CoinActionsProps) => {
const intl = useIntl();
const intl = useIntl();
const _renderItem = (item: string, index: number) => {
const _onPress = () => {
onActionPress(item)
}
return (
<TouchableOpacity key={`action-${item}-${index}`} style={styles.actionContainer} containerStyle={styles.actionBtnContainer} onPress={_onPress}>
<Fragment>
<Text style={styles.actionText}>
{intl.formatMessage({ id: `wallet.${item}` })}
</Text>
</Fragment>
</TouchableOpacity>
)
}
const _renderItem = (item: string, index: number) => {
const _onPress = () => {
onActionPress(item);
};
return (
<View style={styles.actionsContainer}>
{actions.map(_renderItem)}
</View>
)
});
<TouchableOpacity
key={`action-${item}-${index}`}
style={styles.actionContainer}
containerStyle={styles.actionBtnContainer}
onPress={_onPress}
>
<Fragment>
<Text style={styles.actionText}>{intl.formatMessage({ id: `wallet.${item}` })}</Text>
</Fragment>
</TouchableOpacity>
);
};
return <View style={styles.actionsContainer}>{actions.map(_renderItem)}</View>;
});

View File

@ -1,76 +1,77 @@
import React, { Fragment } from 'react'
import React, { Fragment } from 'react';
import { useIntl } from 'react-intl';
import { View, Text, Alert } from 'react-native'
import { View, Text, Alert } from 'react-native';
import { TouchableOpacity } from 'react-native-gesture-handler';
import { DataPair } from '../../../redux/reducers/walletReducer'
import styles from './children.styles'
import { DataPair } from '../../../redux/reducers/walletReducer';
import styles from './children.styles';
interface CoinBasicsProps {
valuePairs: DataPair[];
extraData: DataPair[];
coinSymbol: string;
percentChange: number;
onInfoPress: (id:string)=>void
valuePairs: DataPair[];
extraData: DataPair[];
coinSymbol: string;
percentChange: number;
onInfoPress: (id: string) => void;
}
export const CoinBasics = ({ valuePairs, extraData, coinSymbol, percentChange, onInfoPress}: CoinBasicsProps) => {
const intl = useIntl();
const _renderCoinHeader = (
<>
<View style={styles.coinTitleContainer}>
<Text style={styles.textCoinTitle}>{coinSymbol}</Text>
</View>
<Text
style={styles.textHeaderChange}>
{intl.formatMessage({ id: 'wallet.change' })}
<Text
style={percentChange > 0 ? styles.textPositive : styles.textNegative}>
{` ${percentChange >= 0 ? '+' : ''}${percentChange.toFixed(1)}%`}
</Text>
export const CoinBasics = ({
valuePairs,
extraData,
coinSymbol,
percentChange,
onInfoPress,
}: CoinBasicsProps) => {
const intl = useIntl();
const _renderCoinHeader = (
<>
<View style={styles.coinTitleContainer}>
<Text style={styles.textCoinTitle}>{coinSymbol}</Text>
</View>
<Text style={styles.textHeaderChange}>
{intl.formatMessage({ id: 'wallet.change' })}
<Text style={percentChange > 0 ? styles.textPositive : styles.textNegative}>
{` ${percentChange >= 0 ? '+' : ''}${percentChange.toFixed(1)}%`}
</Text>
</Text>
</>
);
</Text>
</>
)
const _renderValuePair = (args: DataPair, index: number) => {
const label = intl.formatMessage({ id: `wallet.${args.dataKey}` });
return (
<Fragment key={`basic-data-${args.dataKey}-${index}`}>
<Text style={styles.textBasicValue}>{args.value}</Text>
<Text style={styles.textBasicLabel}>{label}</Text>
</Fragment>
);
};
const _renderValuePair = (args: DataPair, index: number) => {
const label = intl.formatMessage({ id: `wallet.${args.dataKey}` })
return (
<Fragment key={`basic-data-${args.dataKey}-${index}`}>
<Text style={styles.textBasicValue}>{args.value}</Text>
<Text style={styles.textBasicLabel}>{label}</Text>
</Fragment>
)
}
const _renderExtraData = (args: DataPair, index: number) => {
const label = intl.formatMessage({ id: `wallet.${args.dataKey || args.labelId}` });
const _renderExtraData = (args: DataPair, index: number) => {
const label = intl.formatMessage({ id: `wallet.${args.dataKey || args.labelId}` })
const _onPress = () => {
onInfoPress(args.dataKey);
}
return (
<View key={`extra-data-${args.dataKey}-${index}`} style={styles.extraDataContainer}>
<Text
style={[styles.textExtraLabel, args.isClickable && styles.textUnderline]}
onPress={args.isClickable && _onPress}>
{label}
</Text>
<Text
style={styles.textExtraValue}
onPress={args.isClickable && _onPress}>
{args.value}
</Text>
</View>
)
}
const _onPress = () => {
onInfoPress(args.dataKey);
};
return (
<View style={[styles.card, styles.basicsContainer]}>
{_renderCoinHeader}
{valuePairs.map(_renderValuePair)}
{extraData && extraData.map(_renderExtraData)}
</View>
)
}
<View key={`extra-data-${args.dataKey}-${index}`} style={styles.extraDataContainer}>
<Text
style={[styles.textExtraLabel, args.isClickable && styles.textUnderline]}
onPress={args.isClickable && _onPress}
>
{label}
</Text>
<Text style={styles.textExtraValue} onPress={args.isClickable && _onPress}>
{args.value}
</Text>
</View>
);
};
return (
<View style={[styles.card, styles.basicsContainer]}>
{_renderCoinHeader}
{valuePairs.map(_renderValuePair)}
{extraData && extraData.map(_renderExtraData)}
</View>
);
};

View File

@ -1,54 +1,50 @@
import React, { useState, useEffect } from 'react'
import { View } from 'react-native'
import React, { useState, useEffect } from 'react';
import { View } from 'react-native';
import { RangeSelector } from '.';
import { SimpleChart } from '../../../components'
import { SimpleChart } from '../../../components';
import { useAppSelector } from '../../../hooks';
import { fetchMarketChart } from '../../../providers/coingecko/coingecko';
import getWindowDimensions from '../../../utils/getWindowDimensions';
import styles, { CHART_NEGATIVE_MARGIN } from './children.styles';
interface CoinChartProps {
coinId:string;
coinId: string;
}
export const CoinChart = ({coinId}:CoinChartProps) => {
const priceHistory = useAppSelector(state=>state.wallet.priceHistories[coinId]);
export const CoinChart = ({ coinId }: CoinChartProps) => {
const priceHistory = useAppSelector((state) => state.wallet.priceHistories[coinId]);
const [range, setRange] = useState(1);
const [chartData, setChartData] = useState(priceHistory?.data);
const _fetchMarketData = async (days:number) => {
const marketData = await fetchMarketChart(coinId, 'usd', days, 'hourly')
setChartData(marketData.prices.map(item=>item.yValue));
}
const _fetchMarketData = async (days: number) => {
const marketData = await fetchMarketChart(coinId, 'usd', days, 'hourly');
setChartData(marketData.prices.map((item) => item.yValue));
};
const _onRangeChange = (range) => {
setRange(range);
_fetchMarketData(range);
}
};
const _renderGraph = () => {
const _baseWidth = getWindowDimensions().width - 32 + CHART_NEGATIVE_MARGIN;
return (
<View style={styles.chartContainer}>
<SimpleChart
data={chartData || []}
baseWidth={_baseWidth} // from react-native
chartHeight={200}
showLine={true}
showLabels={true}
/>
</View>
)}
const _renderGraph = () => {
const _baseWidth = getWindowDimensions().width - 32 + CHART_NEGATIVE_MARGIN;
return (
<>
<View style={styles.card}>
{_renderGraph()}
</View>
<RangeSelector range={range} onRangeChange={_onRangeChange} />
</>
)
}
<View style={styles.chartContainer}>
<SimpleChart
data={chartData || []}
baseWidth={_baseWidth} // from react-native
chartHeight={200}
showLine={true}
showLabels={true}
/>
</View>
);
};
return (
<>
<View style={styles.card}>{_renderGraph()}</View>
<RangeSelector range={range} onRangeChange={_onRangeChange} />
</>
);
};

View File

@ -1,69 +1,61 @@
import React from 'react'
import { View } from 'react-native'
import { CoinActions, CoinBasics, CoinChart } from '.'
import { FormattedCurrency } from '../../../components'
import { COIN_IDS } from '../../../constants/defaultCoins'
import { CoinData, DataPair } from '../../../redux/reducers/walletReducer'
import React from 'react';
import { View } from 'react-native';
import { CoinActions, CoinBasics, CoinChart } from '.';
import { FormattedCurrency } from '../../../components';
import { COIN_IDS } from '../../../constants/defaultCoins';
import { CoinData, DataPair } from '../../../redux/reducers/walletReducer';
export interface CoinSummaryProps {
id:string;
coinSymbol:string;
coinData:CoinData;
percentChagne:number;
onActionPress:(action:string)=>void;
onInfoPress:(dataKey:string)=>void;
id: string;
coinSymbol: string;
coinData: CoinData;
percentChagne: number;
onActionPress: (action: string) => void;
onInfoPress: (dataKey: string) => void;
}
export const CoinSummary = ({
coinSymbol,
id,
coinData,
percentChagne,
onActionPress,
onInfoPress
}:CoinSummaryProps) => {
const {
balance,
estimateValue,
savings,
extraDataPairs,
actions
} = coinData
coinSymbol,
id,
coinData,
percentChagne,
onActionPress,
onInfoPress,
}: CoinSummaryProps) => {
const { balance, estimateValue, savings, extraDataPairs, actions } = coinData;
const valuePairs = [
{
dataKey:'amount_desc',
value:balance
}
] as DataPair[]
const valuePairs = [
{
dataKey: 'amount_desc',
value: balance,
},
] as DataPair[];
if(estimateValue !== undefined){
valuePairs.push({
dataKey:'estimated_value',
value:<FormattedCurrency isApproximate isToken value={estimateValue} />,
})
}
if (estimateValue !== undefined) {
valuePairs.push({
dataKey: 'estimated_value',
value: <FormattedCurrency isApproximate isToken value={estimateValue} />,
});
}
if(savings !== undefined){
valuePairs.push({
dataKey:'savings',
value:savings
})
}
if (savings !== undefined) {
valuePairs.push({
dataKey: 'savings',
value: savings,
});
}
return (
<View>
<CoinBasics
valuePairs={valuePairs}
extraData={extraDataPairs}
coinSymbol={coinSymbol}
percentChange={percentChagne}
onInfoPress={onInfoPress}
/>
<CoinActions actions={actions} onActionPress={onActionPress}/>
{
id !== COIN_IDS.ECENCY && id !== COIN_IDS.HP && <CoinChart coinId={id} />
}
</View>
)
}
return (
<View>
<CoinBasics
valuePairs={valuePairs}
extraData={extraDataPairs}
coinSymbol={coinSymbol}
percentChange={percentChagne}
onInfoPress={onInfoPress}
/>
<CoinActions actions={actions} onActionPress={onActionPress} />
{id !== COIN_IDS.ECENCY && id !== COIN_IDS.HP && <CoinChart coinId={id} />}
</View>
);
};

Some files were not shown because too many files have changed in this diff Show More