diff --git a/src/components/postCard/children/postCardActionsPanel.tsx b/src/components/postCard/children/postCardActionsPanel.tsx index c21795ac03..ac7b060eab 100644 --- a/src/components/postCard/children/postCardActionsPanel.tsx +++ b/src/components/postCard/children/postCardActionsPanel.tsx @@ -35,15 +35,16 @@ export const PostCardActionsPanel = ({ content, handleCardInteraction }: Props) }; const _onReblogsPress = () => { - if (content.reblogs > 0) { - handleCardInteraction(PostCardActionIds.NAVIGATE, { - name: ROUTES.SCREENS.REBLOGS, - params: { - author: content.author, - permlink: content.permlink, - }, - }); - } + const { author, permlink } = content; + + handleCardInteraction(PostCardActionIds.NAVIGATE, { + name: ROUTES.SCREENS.REBLOGS, + params: { + author, + permlink, + }, + }); + }; return ( diff --git a/src/components/postView/container/postDisplayContainer.tsx b/src/components/postView/container/postDisplayContainer.tsx index 5da2f7ead5..c6b257c5fb 100644 --- a/src/components/postView/container/postDisplayContainer.tsx +++ b/src/components/postView/container/postDisplayContainer.tsx @@ -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); diff --git a/src/config/locales/en-US.json b/src/config/locales/en-US.json index e946c9dc57..7168073bf5 100644 --- a/src/config/locales/en-US.json +++ b/src/config/locales/en-US.json @@ -960,7 +960,9 @@ "time": "TIME" }, "reblog": { - "title": "Reblog Info" + "title": "Reblog Info", + "reblog_post": "Reblog Post", + "reblog_delete": "Undo Reblog" }, "dsteem": { "date_error": { diff --git a/src/globalStyles.js b/src/globalStyles.js index 6195973863..da6644ba53 100644 --- a/src/globalStyles.js +++ b/src/globalStyles.js @@ -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, + }, }); diff --git a/src/providers/hive/dhive.js b/src/providers/hive/dhive.js index 5241d95bc5..6c01309dc6 100644 --- a/src/providers/hive/dhive.js +++ b/src/providers/hive/dhive.js @@ -412,7 +412,7 @@ export const getUser = async (user, loggedIn = true) => { export const getUserReputation = async (author) => { try { const response = await client.call('condenser_api', 'get_account_reputations', [author, 1]); - + if (response && response.length < 1) { return 0; } @@ -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([ - 'reblog', - { - account: follower, - author, - permlink, - }, - ]), - required_auths: [], - required_posting_auths: [follower], - }; + const json = [ + 'reblog', + { + account: account.name, + author, + permlink, + delete: undo ? 'delete' : undefined + }, + ]; - 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); diff --git a/src/providers/queries/postQueries/index.ts b/src/providers/queries/postQueries/index.ts index 30939a271c..ad945efaeb 100644 --- a/src/providers/queries/postQueries/index.ts +++ b/src/providers/queries/postQueries/index.ts @@ -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 }; diff --git a/src/providers/queries/postQueries/reblogQueries.ts b/src/providers/queries/postQueries/reblogQueries.ts new file mode 100644 index 0000000000..8e40c0fab2 --- /dev/null +++ b/src/providers/queries/postQueries/reblogQueries.ts @@ -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( + [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["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["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' }))); + } + } + } + + + }); +} + + diff --git a/src/providers/queries/queryKeys.ts b/src/providers/queries/queryKeys.ts index acd03a84d7..3ac50c877a 100644 --- a/src/providers/queries/queryKeys.ts +++ b/src/providers/queries/queryKeys.ts @@ -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', diff --git a/src/screens/reblogs/screen/reblogScreen.js b/src/screens/reblogs/screen/reblogScreen.js deleted file mode 100644 index f47a86ae07..0000000000 --- a/src/screens/reblogs/screen/reblogScreen.js +++ /dev/null @@ -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 ( - 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 ( - - {({ data, filterResult, handleSearch, handleOnUserPress }) => ( - - handleSearch(text, 'account')} - /> - item.account} - removeClippedSubviews={false} - renderItem={({ item, index }) => renderUserListItem(item, index, handleOnUserPress)} - /> - - )} - - ); -}; - -export default gestureHandlerRootHOC(ReblogScreen); diff --git a/src/screens/reblogs/screen/reblogScreen.tsx b/src/screens/reblogs/screen/reblogScreen.tsx new file mode 100644 index 0000000000..a319cb567a --- /dev/null +++ b/src/screens/reblogs/screen/reblogScreen.tsx @@ -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 ( + 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 ( + + + + ); + }; + + + + + return ( + + {({ data, filterResult, handleSearch, handleOnUserPress }) => ( + + + + {/* Your content goes here */} + handleSearch(text, 'account')} + /> + item.account} + removeClippedSubviews={false} + renderItem={({ item, index }) => + renderUserListItem(item, index, handleOnUserPress) + } + refreshControl={ + reblogsQuery.refetch()} + progressBackgroundColor="#357CE6" + tintColor={!isDarkTheme ? '#357ce6' : '#96c0ff'} + titleColor="#fff" + colors={['#fff']} + />} + /> + + {_renderFloatingButton()} + + + ) + } + + ); +}; + +export default gestureHandlerRootHOC(ReblogScreen); diff --git a/src/screens/reblogs/styles/reblogScreen.styles.ts b/src/screens/reblogs/styles/reblogScreen.styles.ts new file mode 100644 index 0000000000..62f422a3e4 --- /dev/null +++ b/src/screens/reblogs/styles/reblogScreen.styles.ts @@ -0,0 +1,9 @@ +import EStyleSheet from 'react-native-extended-stylesheet'; + +export default EStyleSheet.create({ + floatingContainer: { + position: 'absolute', + right: 16, + bottom: 56, + } +}) \ No newline at end of file