Merge pull request #2720 from ecency/mb/reblog

add a reblog functionality into reblogScreen
This commit is contained in:
Feruz M 2024-05-16 15:57:20 +03:00 committed by GitHub
commit 06deb8ed0c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 329 additions and 135 deletions

View File

@ -35,15 +35,16 @@ export const PostCardActionsPanel = ({ content, handleCardInteraction }: Props)
};
const _onReblogsPress = () => {
if (content.reblogs > 0) {
const { author, permlink } = content;
handleCardInteraction(PostCardActionIds.NAVIGATE, {
name: ROUTES.SCREENS.REBLOGS,
params: {
author: content.author,
permlink: content.permlink,
author,
permlink,
},
});
}
};
return (

View File

@ -71,8 +71,9 @@ const PostDisplayContainer = ({
navigation.navigate({
name: ROUTES.SCREENS.REBLOGS,
params: {
author: post.author,
permlink: post.permlink,
reblogs,
author,
permlink,
},
key: post.permlink + post.reblogs.length,
} as never);

View File

@ -960,7 +960,9 @@
"time": "TIME"
},
"reblog": {
"title": "Reblog Info"
"title": "Reblog Info",
"reblog_post": "Reblog Post",
"reblog_delete": "Undo Reblog"
},
"dsteem": {
"date_error": {

View File

@ -1,4 +1,5 @@
import EStyleSheet from 'react-native-extended-stylesheet';
import { FlipInEasyX } from 'react-native-reanimated';
export default EStyleSheet.create({
containerHorizontal16: {
@ -87,4 +88,11 @@ export default EStyleSheet.create({
tabBarBottom: {
paddingBottom: 60,
},
mainbutton: {
width: '40%',
justifyContent: 'center',
position: 'relative',
left: 180,
bottom: 20,
},
});

View File

@ -1751,62 +1751,30 @@ const _postContent = async (
// Re-blog
// TODO: remove pinCode
export const reblog = (account, pinCode, author, permlink) =>
_reblog(account, pinCode, author, permlink).then((resp) => {
export const reblog = (account, pinCode, author, permlink, undo = false) =>
_reblog(account, pinCode, author, permlink, undo).then((resp) => {
return resp;
});
const _reblog = async (account, pinCode, author, permlink) => {
const pin = getDigitPinCode(pinCode);
const key = getAnyPrivateKey(account.local, pin);
if (account.local.authType === AUTH_TYPE.STEEM_CONNECT) {
const token = decryptKey(account.local.accessToken, pin);
const api = new hsClient({
accessToken: token,
});
const _reblog = async (account, pinCode, author, permlink, undo = false) => {
const follower = account.name;
return api.reblog(follower, author, permlink).then((resp) => resp.result);
}
if (key) {
const privateKey = PrivateKey.fromString(key);
const follower = account.name;
const json = {
id: 'follow',
json: jsonStringify([
const json = [
'reblog',
{
account: follower,
account: account.name,
author,
permlink,
delete: undo ? 'delete' : undefined
},
]),
required_auths: [],
required_posting_auths: [follower],
};
];
const opArray = [['custom_json', json]];
return new Promise((resolve, reject) => {
sendHiveOperations(opArray, privateKey)
.then((result) => {
resolve(result);
})
.catch((err) => {
reject(err);
});
});
}
return broadcastPostingJSON('follow', json, account, pinCode)
return Promise.reject(
new Error('Check private key permission! Required private posting key or above.'),
);
};
export const claimRewardBalance = (account, pinCode, rewardHive, rewardHbd, rewardVests) => {
const pin = getDigitPinCode(pinCode);
const key = getAnyPrivateKey(get(account, 'local'), pin);

View File

@ -1,5 +1,6 @@
import * as postQueries from './postQueries';
import * as wavesQueries from './wavesQueries';
import * as pollQueries from './pollQueries';
import * as reblogQueries from './reblogQueries';
export { postQueries, wavesQueries, pollQueries };
export { postQueries, wavesQueries, pollQueries, reblogQueries };

View File

@ -0,0 +1,128 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import QUERIES from "../queryKeys";
import { useAppSelector } from "../../../hooks";
import { useDispatch } from "react-redux";
import { setRcOffer, toastNotification } from "../../../redux/actions/uiAction";
import { useIntl } from "react-intl";
import { getPostReblogs, reblog } from "../../hive/dhive";
import { get } from 'lodash';
import { PointActivityIds } from "../../ecency/ecency.types";
import { useUserActivityMutation } from "../pointQueries";
/** hook used to return post poll */
export const useGetReblogsQuery = (author: string, permlink: string) => {
const query = useQuery<string[]>(
[QUERIES.POST.GET_REBLOGS, author, permlink],
async () => {
if (!author || !permlink) {
return null;
}
try {
const reblogs = await getPostReblogs(author, permlink);
if (!reblogs) {
new Error('Reblog data unavailable');
}
return reblogs
} catch (err) {
console.warn('Failed to get post', err);
return []
}
},
{
initialData: [],
cacheTime: 30 * 60 * 1000, // keeps cache for 30 minutes
},
);
return query;
};
export function useReblogMutation(author: string, permlink: string) {
// const { activeUser } = useMappedStore();
const intl = useIntl();
const dispatch = useDispatch();
const queryClient = useQueryClient()
const currentAccount = useAppSelector(state => state.account.currentAccount);
const pinHash = useAppSelector(state => state.application.pin);
const userActivityMutation = useUserActivityMutation();
return useMutation({
mutationKey: [QUERIES.POST.REBLOG_POST],
mutationFn: async ({ undo }:{undo:boolean}) => {
if (!author || !permlink || !currentAccount) {
throw new Error("Not enough data to reblog post")
}
const resp = await reblog(currentAccount, pinHash, author, permlink, undo)
// track user activity points ty=130
userActivityMutation.mutate({
pointsTy: PointActivityIds.REBLOG,
transactionId: resp.id,
});
return resp;
},
retry: 3,
onSuccess: (resp, vars) => {
console.log("reblog response", resp);
//update poll cache here
queryClient.setQueryData<ReturnType<typeof useGetReblogsQuery>["data"]>(
[QUERIES.POST.GET_REBLOGS, author, permlink],
(data) => {
if (!data || !resp) {
return data;
}
const _curIndex = data.indexOf(currentAccount.username)
if(vars.undo){
data.splice(_curIndex, 1)
}
else if (_curIndex < 0) {
data.splice(0, 0, currentAccount.username);
}
return [...data] as ReturnType<typeof useGetReblogsQuery>["data"];
}
)
},
onError: (error) => {
if (String(get(error, 'jse_shortmsg', '')).indexOf('has already reblogged') > -1) {
dispatch(
toastNotification(
intl.formatMessage({
id: 'alert.already_rebloged',
}),
),
);
} else {
if (error && error.jse_shortmsg.split(': ')[1].includes('wait to transact')) {
// when RC is not enough, offer boosting account
dispatch(setRcOffer(true));
} else {
// when other errors
dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' })));
}
}
}
});
}

View File

@ -28,7 +28,9 @@ const QUERIES = {
GET: 'QUERY_GET_POST',
GET_POLL: 'QUERY_GET_POLL',
GET_DISCUSSION: 'QUERY_GET_DISCUSSION',
SIGN_POLL_VOTE:"SIGN_POLL_VOTE"
SIGN_POLL_VOTE:"SIGN_POLL_VOTE",
GET_REBLOGS:"GET_REBLOGS",
REBLOG_POST:"REBLOG_POST"
},
LEADERBOARD: {
GET: 'QUERY_GET_LEADERBOARD',

View File

@ -1,74 +0,0 @@
import React, { useEffect, useState } from 'react';
import { FlatList, SafeAreaView } from 'react-native';
import { useIntl } from 'react-intl';
// Components
import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
import { BasicHeader, UserListItem } from '../../../components';
// Container
import AccountListContainer from '../../../containers/accountListContainer';
// Utils
import globalStyles from '../../../globalStyles';
import { getTimeFromNow } from '../../../utils/time';
import { getPostReblogs } from '../../../providers/hive/dhive';
const renderUserListItem = (item, index, handleOnUserPress) => {
return (
<UserListItem
index={index}
username={item.account}
description={getTimeFromNow(item.timestamp)}
handleOnPress={() => handleOnUserPress(item.account)}
isClickable
/>
);
};
const ReblogScreen = ({ route }) => {
const intl = useIntl();
const headerTitle = intl.formatMessage({
id: 'reblog.title',
});
const [reblogs, setReblogs] = useState([]);
useEffect(() => {
_fetchReblogs();
}, []);
const _fetchReblogs = async () => {
const author = route.params?.author;
const permlink = route.params?.permlink;
if (author && permlink) {
const _reblogs = await getPostReblogs(author, permlink);
setReblogs(_reblogs.map((account) => ({ account })));
}
};
return (
<AccountListContainer data={reblogs}>
{({ data, filterResult, handleSearch, handleOnUserPress }) => (
<SafeAreaView style={[globalStyles.container, { paddingBottom: 40 }]}>
<BasicHeader
title={`${headerTitle} (${data && data.length})`}
backIconName="close"
isHasSearch
handleOnSearch={(text) => handleSearch(text, 'account')}
/>
<FlatList
data={filterResult || data}
keyExtractor={(item) => item.account}
removeClippedSubviews={false}
renderItem={({ item, index }) => renderUserListItem(item, index, handleOnUserPress)}
/>
</SafeAreaView>
)}
</AccountListContainer>
);
};
export default gestureHandlerRootHOC(ReblogScreen);

View File

@ -0,0 +1,148 @@
import React, { useMemo, useState } from 'react';
import { FlatList, RefreshControl, SafeAreaView } from 'react-native';
import { useIntl } from 'react-intl';
import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
import { useAppSelector } from '../../../hooks';
import showLoginAlert from '../../../utils/showLoginAlert';
// Components
import { BasicHeader, MainButton, UserListItem } from '../../../components';
// Container
import AccountListContainer from '../../../containers/accountListContainer';
// Utils
import globalStyles from '../../../globalStyles';
import styles from '../styles/reblogScreen.styles';
import { getTimeFromNow } from '../../../utils/time';
import Animated, { BounceInRight } from 'react-native-reanimated';
import { reblogQueries } from '../../../providers/queries';
;
const renderUserListItem = (item, index, handleOnUserPress) => {
return (
<UserListItem
index={index}
username={item.account}
description={getTimeFromNow(item.timestamp)}
handleOnPress={() => handleOnUserPress(item.account)}
/>
);
};
const ReblogScreen = ({ route }) => {
const intl = useIntl();
const author = route.params?.author;
const permlink = route.params?.permlink;
const currentAccount = useAppSelector((state) => state.account.currentAccount);
const isLoggedIn = useAppSelector((state) => state.application.isLoggedIn);
const isDarkTheme = useAppSelector((state) => state.application.isDarkTheme);
const [isReblogging, setIsReblogging] = useState(false);
const reblogsQuery = reblogQueries.useGetReblogsQuery(author, permlink);
const reblogMutation = reblogQueries.useReblogMutation(author, permlink);
//map reblogs data for account list
const { reblogs, deleteEnabled } = useMemo(() => {
let _reblogs: any[] = [];
let _deleteEnabled = false;
if (reblogsQuery.data instanceof Array) {
_reblogs = reblogsQuery.data.map((username) => ({ account: username }));
_deleteEnabled = currentAccount ? reblogsQuery.data.includes(currentAccount.username) : false;
}
return {
reblogs: _reblogs,
deleteEnabled: _deleteEnabled
}
}, [reblogsQuery.data?.length])
const headerTitle = intl.formatMessage({
id: 'reblog.title',
});
const _actionBtnTitle = intl.formatMessage({ id: deleteEnabled ? 'reblog.reblog_delete' : 'reblog.reblog_post' })
const _actionBtnIcon = deleteEnabled ? "repeat-off" : "repeat";
const _handleReblogPost = async () => {
if (!isLoggedIn) {
showLoginAlert({ intl });
return;
}
if (isLoggedIn) {
setIsReblogging(true);
await reblogMutation.mutateAsync({ undo:deleteEnabled });
setIsReblogging(false);
}
}
const _renderFloatingButton = () => {
return (
<Animated.View style={styles.floatingContainer} entering={BounceInRight.delay(300)}>
<MainButton
onPress={_handleReblogPost}
iconName={_actionBtnIcon}
iconType="MaterialCommunityIcons"
iconColor="white"
text={_actionBtnTitle}
isLoading={isReblogging}
/>
</Animated.View>
);
};
return (
<AccountListContainer data={reblogs}>
{({ data, filterResult, handleSearch, handleOnUserPress }) => (
<SafeAreaView style={[globalStyles.container, { paddingBottom: 40 }]}>
{/* Your content goes here */}
<BasicHeader
title={`${headerTitle} (${data && data.length})`}
backIconName="close"
isHasSearch
handleOnSearch={(text) => handleSearch(text, 'account')}
/>
<FlatList
data={filterResult || data}
keyExtractor={(item) => item.account}
removeClippedSubviews={false}
renderItem={({ item, index }) =>
renderUserListItem(item, index, handleOnUserPress)
}
refreshControl={
<RefreshControl
refreshing={reblogsQuery.isLoading || reblogsQuery.isFetching}
onRefresh={() => reblogsQuery.refetch()}
progressBackgroundColor="#357CE6"
tintColor={!isDarkTheme ? '#357ce6' : '#96c0ff'}
titleColor="#fff"
colors={['#fff']}
/>}
/>
{_renderFloatingButton()}
</SafeAreaView>
)
}
</AccountListContainer >
);
};
export default gestureHandlerRootHOC(ReblogScreen);

View File

@ -0,0 +1,9 @@
import EStyleSheet from 'react-native-extended-stylesheet';
export default EStyleSheet.create({
floatingContainer: {
position: 'absolute',
right: 16,
bottom: 56,
}
})