diff --git a/package.json b/package.json index 10f836dc7..41b31a112 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,9 @@ "@react-navigation/native": "^6.0.11", "@react-navigation/native-stack": "^6.7.0", "@react-navigation/stack": "^6.2.2", + "@tanstack/query-async-storage-persister": "^4.3.9", + "@tanstack/react-query": "^4.3.9", + "@tanstack/react-query-persist-client": "^4.3.9", "@tradle/react-native-http": "^2.0.0", "appcenter": "^4.1.0", "appcenter-analytics": "^4.1.0", @@ -104,7 +107,6 @@ "react-native-highlight-words": "^1.0.1", "react-native-iap": "^7.5.6", "react-native-image-crop-picker": "^0.35.2", - "react-native-image-size": "^1.1.3", "react-native-image-zoom-viewer": "^2.2.27", "react-native-iphone-x-helper": "^1.3.1", "react-native-keyboard-aware-scroll-view": "^0.9.1", diff --git a/src/components/draftListItem/view/draftListItemStyles.js b/src/components/draftListItem/view/draftListItemStyles.js index b19d46b67..cd7420479 100644 --- a/src/components/draftListItem/view/draftListItemStyles.js +++ b/src/components/draftListItem/view/draftListItemStyles.js @@ -14,16 +14,9 @@ export default EStyleSheet.create({ body: { marginHorizontal: 9, }, - image: { - margin: 0, - alignItems: 'center', - alignSelf: 'center', - //height: 200, - //width: '$deviceWidth - 16', - borderRadius: 8, - backgroundColor: '$primaryLightGray', - // paddingVertical: 10, - marginVertical: 5, + thumbnail: { + width: '$deviceWidth - 16', + height: 300 }, postDescripton: { flexDirection: 'column', diff --git a/src/components/draftListItem/view/draftListItemView.js b/src/components/draftListItem/view/draftListItemView.tsx similarity index 68% rename from src/components/draftListItem/view/draftListItemView.js rename to src/components/draftListItem/view/draftListItemView.tsx index c096c5282..60c5de868 100644 --- a/src/components/draftListItem/view/draftListItemView.js +++ b/src/components/draftListItem/view/draftListItemView.tsx @@ -1,7 +1,6 @@ import React, { useRef, useState, useEffect, Fragment } from 'react'; import { View, Text, TouchableOpacity } from 'react-native'; import { injectIntl } from 'react-intl'; -import ImageSize from 'react-native-image-size'; // Utils import { getTimeFromNow } from '../../../utils/time'; @@ -9,20 +8,14 @@ import { getTimeFromNow } from '../../../utils/time'; // Components import { PostHeaderDescription } from '../../postElements'; import { IconButton } from '../../iconButton'; -import ProgressiveImage from '../../progressiveImage'; import { OptionsModal } from '../../atoms'; // Styles import styles from './draftListItemStyles'; import { ScheduledPostStatus } from '../../../providers/ecency/ecency.types'; import { PopoverWrapper } from '../../popoverWrapper/popoverWrapperView'; -import getWindowDimensions from '../../../utils/getWindowDimensions'; +import FastImage from 'react-native-fast-image'; -// Defaults -const DEFAULT_IMAGE = - 'https://images.ecency.com/DQmT8R33geccEjJfzZEdsRHpP3VE8pu3peRCnQa1qukU4KR/no_image_3x.png'; - -const dim = getWindowDimensions(); const DraftListItemView = ({ title, @@ -35,52 +28,55 @@ const DraftListItemView = ({ thumbnail, handleOnPressItem, handleOnRemoveItem, + handleOnMovePress, id, intl, isFormatedDate, status, isSchedules, + isDeleting, }) => { const actionSheet = useRef(null); - const [calcImgHeight, setCalcImgHeight] = useState(300); - // Component Life Cycles + const moveActionSheet = useRef(null); + const [deleteRequested, setIsDeleteRequested] = useState(false); useEffect(() => { - let _isMounted = false; - if (image) { - if (!_isMounted) { - ImageSize.getSize(thumbnail.uri).then((size) => { - setCalcImgHeight(Math.floor((size.height / size.width) * dim.width)); - }); - } + if (deleteRequested && !isDeleting) { + setIsDeleteRequested(false); } - return () => { - _isMounted = true; - }; - }, []); + }, [isDeleting]); + + const _onItemPress = () => { + if (isSchedules) { + moveActionSheet.current.show(); + return; + } + + handleOnPressItem(id); + }; // consts const scheduleStatus = status === ScheduledPostStatus.PENDING ? intl.formatMessage({ id: 'schedules.pending' }) : status === ScheduledPostStatus.POSTPONED - ? intl.formatMessage({ id: 'schedules.postponed' }) - : status === ScheduledPostStatus.PUBLISHED - ? intl.formatMessage({ id: 'schedules.published' }) - : intl.formatMessage({ id: 'schedules.error' }); + ? intl.formatMessage({ id: 'schedules.postponed' }) + : status === ScheduledPostStatus.PUBLISHED + ? intl.formatMessage({ id: 'schedules.published' }) + : intl.formatMessage({ id: 'schedules.error' }); const statusIcon = status === ScheduledPostStatus.PENDING ? 'timer' : status === ScheduledPostStatus.POSTPONED - ? 'schedule' - : status === ScheduledPostStatus.PUBLISHED - ? 'check-circle' - : 'error'; + ? 'schedule' + : status === ScheduledPostStatus.PUBLISHED + ? 'check-circle' + : 'error'; const statusIconColor = status === ScheduledPostStatus.PUBLISHED ? '#4FD688' : status === ScheduledPostStatus.ERROR - ? '#e63535' - : '#c1c5c7'; + ? '#e63535' + : '#c1c5c7'; return ( @@ -116,19 +112,17 @@ const DraftListItemView = ({ onPress={() => actionSheet.current.show()} style={[styles.rightItem]} color="#c1c5c7" + isLoading={isDeleting && deleteRequested} /> - handleOnPressItem(id)}> + {image !== null && ( - )} @@ -151,6 +145,29 @@ const DraftListItemView = ({ onPress={(index) => { if (index === 0) { handleOnRemoveItem(id); + setIsDeleteRequested(true); + } + }} + /> + + { + if (index === 0) { + handleOnMovePress(id); + setIsDeleteRequested(true); } }} /> diff --git a/src/components/editorElements/tagInput/view/tagInputStyles.js b/src/components/editorElements/tagInput/view/tagInputStyles.js index c39061ddf..c23d0b55b 100644 --- a/src/components/editorElements/tagInput/view/tagInputStyles.js +++ b/src/components/editorElements/tagInput/view/tagInputStyles.js @@ -2,9 +2,7 @@ import EStyleSheet from 'react-native-extended-stylesheet'; import isAndroidOreo from '../../../../utils/isAndroidOreo'; export default EStyleSheet.create({ - container: { - - }, + container: {}, textInput: { color: '$primaryBlack', fontSize: 15, @@ -14,7 +12,7 @@ export default EStyleSheet.create({ borderTopColor: '$primaryLightGray', borderBottomWidth: 1, borderBottomColor: '$primaryLightGray', - height: isAndroidOreo() ? 36: 40 + height: isAndroidOreo() ? 36 : 40, }, warning: { color: '$primaryRed', diff --git a/src/components/markdownEditor/styles/markdownEditorStyles.js b/src/components/markdownEditor/styles/markdownEditorStyles.js index 7811eb335..562210877 100644 --- a/src/components/markdownEditor/styles/markdownEditorStyles.js +++ b/src/components/markdownEditor/styles/markdownEditorStyles.js @@ -17,7 +17,7 @@ export default EStyleSheet.create({ color: '$primaryBlack', backgroundColor: '$primaryBackgroundColor', textAlignVertical: 'top', - maxHeight: isAndroidOreo() ? '$deviceHeight':undefined + maxHeight: isAndroidOreo() ? '$deviceHeight' : undefined, }, previewContainer: { flex: 1, diff --git a/src/index.js b/src/index.js index da309d46f..a4eae33b1 100755 --- a/src/index.js +++ b/src/index.js @@ -5,22 +5,28 @@ import { PersistGate } from 'redux-persist/integration/react'; import { IntlProvider } from 'react-intl'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { Host } from 'react-native-portalize'; +import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'; import { flattenMessages } from './utils/flattenMessages'; import messages from './config/locales'; import Application from './screens/application'; import { store, persistor } from './redux/store/store'; +import { initQueryClient } from './providers/queries'; + +const queryClientProviderProps = initQueryClient(); const _renderApp = ({ locale }) => ( - - - - - - - - - + + + + + + + + + + + ); const mapStateToProps = (state) => ({ diff --git a/src/providers/ecency/ecency.ts b/src/providers/ecency/ecency.ts index fead824f0..2c4812a48 100644 --- a/src/providers/ecency/ecency.ts +++ b/src/providers/ecency/ecency.ts @@ -6,14 +6,23 @@ import { upload } from '../../config/imageApi'; import serverList from '../../config/serverListApi'; import { SERVER_LIST } from '../../constants/options/api'; import { parsePost } from '../../utils/postParser'; -import { convertCommentHistory, convertLatestQuotes, convertReferral, convertReferralStat } from './converters'; -import { CommentHistoryItem, LatestMarketPrices, ReceivedVestingShare, Referral, ReferralStat } from './ecency.types'; +import { + convertCommentHistory, + convertLatestQuotes, + convertReferral, + convertReferralStat, +} from './converters'; +import { + CommentHistoryItem, + LatestMarketPrices, + ReceivedVestingShare, + Referral, + ReferralStat, +} from './ecency.types'; - - -/** +/** * ************************************ - * CURRENCY APIS IMPLEMENTATION + * CURRENCY APIS IMPLEMENTATION * ************************************ */ @@ -30,10 +39,10 @@ export const getCurrencyRate = (currency) => export const getLatestQuotes = async (currencyRate: number): Promise => { try { console.log('using currency rate', currencyRate); - const res = await ecencyApi.get(`/private-api/market-data/latest`); + const res = await ecencyApi.get('/private-api/market-data/latest'); if (!res.data) { - throw new Error("No quote data returned"); + throw new Error('No quote data returned'); } const data = convertLatestQuotes(res.data, currencyRate); @@ -43,10 +52,9 @@ export const getLatestQuotes = async (currencyRate: number): Promise ecencyApi @@ -57,27 +65,22 @@ export const getCurrencyTokenRate = (currency, token) => return 0; }); - -export const getReceivedVestingShares = async (username: string): Promise => { +export const getReceivedVestingShares = async ( + username: string, +): Promise => { try { const res = await ecencyApi.get(`/private-api/received-vesting/${username}`); - console.log("Vesting Shares User", username, res.data); + console.log('Vesting Shares User', username, res.data); if (!res.data || !res.data.list) { - throw new Error("No vesting shares for user") + throw new Error('No vesting shares for user'); } return res.data.list; } catch (error) { bugsnagInstance.notify(error); console.warn(error); - throw error + throw error; } -} - - - - - - +}; /** * returns list of saved drafts on ecency server @@ -85,29 +88,26 @@ export const getReceivedVestingShares = async (username: string): Promise { try { const res = await ecencyApi.post('/private-api/drafts'); - return res.data; + return res.data || []; } catch (error) { bugsnagInstance.notify(error); throw error; } -} - - +}; /** * @params draftId */ export const deleteDraft = async (draftId: string) => { try { - const data = { id: draftId } - const res = await ecencyApi.post(`/private-api/drafts-delete`, data); - return res.data + const data = { id: draftId }; + const res = await ecencyApi.post('/private-api/drafts-delete', data); + return res.data || []; } catch (error) { bugsnagInstance.notify(error); throw error; } -} - +}; /** * @params title @@ -117,8 +117,8 @@ export const deleteDraft = async (draftId: string) => { */ export const addDraft = async (title: string, body: string, tags: string, meta: Object) => { try { - const data = { title, body, tags, meta } - const res = await ecencyApi.post('/private-api/drafts-add', data) + const data = { title, body, tags, meta }; + const res = await ecencyApi.post('/private-api/drafts-add', data); const { drafts } = res.data; if (drafts) { return drafts.pop(); //return recently saved last draft in the list @@ -129,8 +129,7 @@ export const addDraft = async (title: string, body: string, tags: string, meta: bugsnagInstance.notify(error); throw error; } -} - +}; /** * @params draftId @@ -139,14 +138,20 @@ export const addDraft = async (title: string, body: string, tags: string, meta: * @params tags * @params meta */ -export const updateDraft = async (draftId: string, title: string, body: string, tags: string, meta: Object) => { +export const updateDraft = async ( + draftId: string, + title: string, + body: string, + tags: string, + meta: Object, +) => { try { - const data = { id: draftId, title, body, tags, meta } - const res = await ecencyApi.post(`/private-api/drafts-update`, data) + const data = { id: draftId, title, body, tags, meta }; + const res = await ecencyApi.post('/private-api/drafts-update', data); if (res.data) { - return res.data + return res.data; } else { - throw new Error("No data returned in response") + throw new Error('No data returned in response'); } } catch (error) { bugsnagInstance.notify(error); @@ -154,31 +159,29 @@ export const updateDraft = async (draftId: string, title: string, body: string, } }; - - -/** +/** * ************************************ - * BOOKMARKS ECENCY APIS IMPLEMENTATION + * BOOKMARKS ECENCY APIS IMPLEMENTATION * ************************************ */ /** * Adds post to user's bookmarks - * @param author - * @param permlink + * @param author + * @param permlink * @returns array of saved bookmarks */ export const addBookmark = async (author: string, permlink: string) => { try { const data = { author, permlink }; - const response = await ecencyApi.post(`/private-api/bookmarks-add`, data); + const response = await ecencyApi.post('/private-api/bookmarks-add', data); return response.data; } catch (error) { - console.warn("Failed to add bookmark", error) - bugsnagInstance.notify(error) - throw error + console.warn('Failed to add bookmark', error); + bugsnagInstance.notify(error); + throw error; } -} +}; /** * fetches saved bookmarks of user @@ -186,15 +189,14 @@ export const addBookmark = async (author: string, permlink: string) => { */ export const getBookmarks = async () => { try { - const response = await ecencyApi.post(`/private-api/bookmarks`); + const response = await ecencyApi.post('/private-api/bookmarks'); return response.data; } catch (error) { - console.warn("Failed to get saved bookmarks", error) - bugsnagInstance.notify(error) - throw error + console.warn('Failed to get saved bookmarks', error); + bugsnagInstance.notify(error); + throw error; } -} - +}; /** * Deletes bookmark from user's saved bookmarks @@ -203,52 +205,48 @@ export const getBookmarks = async () => { */ export const deleteBookmark = async (bookmarkId: string) => { try { - const data = { id: bookmarkId } - const response = await ecencyApi.post(`/private-api/bookmarks-delete`, data); + const data = { id: bookmarkId }; + const response = await ecencyApi.post('/private-api/bookmarks-delete', data); return response.data; } catch (error) { - console.warn("Failed to delete bookmark", error) - bugsnagInstance.notify(error) - throw error + console.warn('Failed to delete bookmark', error); + bugsnagInstance.notify(error); + throw error; } -} - +}; export const addReport = async (type: 'content' | 'user', data: string) => { try { - const response = await api - .post('/report', { - type, - data - }) - return response.data + const response = await api.post('/report', { + type, + data, + }); + return response.data; } catch (err) { - console.warn("Failed to report to ecency") + console.warn('Failed to report to ecency'); bugsnagInstance.notify(err); throw err; } -} +}; export const deleteAccount = async (username: string) => { try { - const response = await api - .post('/request-delete', { - username, - }) - return response.data + const response = await api.post('/request-delete', { + username, + }); + return response.data; } catch (err) { - console.warn("Failed to report to ecency") + console.warn('Failed to report to ecency'); bugsnagInstance.notify(err); throw err; } -} +}; - -/** -* ************************************ -* FAVOURITES ECENCY APIS IMPLEMENTATION -* ************************************ -*/ +/** + * ************************************ + * FAVOURITES ECENCY APIS IMPLEMENTATION + * ************************************ + */ /** * Fetches user favourites @@ -256,14 +254,14 @@ export const deleteAccount = async (username: string) => { */ export const getFavorites = async () => { try { - const response = await ecencyApi.post(`/private-api/favorites`) + const response = await ecencyApi.post('/private-api/favorites'); return response.data; } catch (error) { - console.warn("Failed to get favorites", error); + console.warn('Failed to get favorites', error); bugsnagInstance.notify(error); - throw error + throw error; } -} +}; /** * Checks if user is precent in current user's favourites @@ -273,13 +271,13 @@ export const getFavorites = async () => { export const checkFavorite = async (targetUsername: string) => { try { const data = { account: targetUsername }; - const response = await ecencyApi.post(`/private-api/favorites-check`, data); + const response = await ecencyApi.post('/private-api/favorites-check', data); return response.data || false; } catch (error) { - console.warn("Failed to check favorite", error); + console.warn('Failed to check favorite', error); bugsnagInstance.notify(error); } -} +}; /** * Adds taget user to current user's favourites @@ -289,15 +287,14 @@ export const checkFavorite = async (targetUsername: string) => { export const addFavorite = async (targetUsername: string) => { try { const data = { account: targetUsername }; - const response = await ecencyApi.post(`/private-api/favorites-add`, data); + const response = await ecencyApi.post('/private-api/favorites-add', data); return response.data; } catch (error) { - console.warn("Failed to add user favorites", error); + console.warn('Failed to add user favorites', error); bugsnagInstance.notify(error); - throw error + throw error; } -} - +}; /** * Removes taget user to current user's favourites @@ -307,38 +304,35 @@ export const addFavorite = async (targetUsername: string) => { export const deleteFavorite = async (targetUsername: string) => { try { const data = { account: targetUsername }; - const response = await ecencyApi.post(`/private-api/favorites-delete`, data); + const response = await ecencyApi.post('/private-api/favorites-delete', data); return response.data; } catch (error) { - console.warn("Failed to add user favorites", error); + console.warn('Failed to add user favorites', error); bugsnagInstance.notify(error); throw error; } -} +}; - -/** +/** * ************************************ - * SNIPPETS ECENCY APIS IMPLEMENTATION + * SNIPPETS ECENCY APIS IMPLEMENTATION * ************************************ */ - /** * Fetches all saved user fragments/snippets from ecency * @returns array of fragments */ export const getFragments = async () => { try { - const response = await ecencyApi.post(`/private-api/fragments`); + const response = await ecencyApi.post('/private-api/fragments'); return response.data; } catch (error) { - console.warn("Failed to get fragments", error); - bugsnagInstance.notify(error) + console.warn('Failed to get fragments', error); + bugsnagInstance.notify(error); throw error; } -} - +}; /** * Adds new fragment/snippets to user's saved fragments/snippets @@ -350,14 +344,14 @@ export const getFragments = async () => { export const addFragment = async (title: string, body: string) => { try { const data = { title, body }; - const response = await ecencyApi.post(`/private-api/fragments-add`, data); + const response = await ecencyApi.post('/private-api/fragments-add', data); return response.data; } catch (error) { - console.warn("Failed to add fragment", error); - bugsnagInstance.notify(error) + console.warn('Failed to add fragment', error); + bugsnagInstance.notify(error); throw error; } -} +}; /** * Updates a fragment content using fragment id @@ -369,14 +363,14 @@ export const addFragment = async (title: string, body: string) => { export const updateFragment = async (fragmentId: string, title: string, body: string) => { try { const data = { id: fragmentId, title, body }; - const response = await ecencyApi.post(`/private-api/fragments-update`, data); + const response = await ecencyApi.post('/private-api/fragments-update', data); return response.data; } catch (error) { - console.warn("Failed to update fragment", error); - bugsnagInstance.notify(error) + console.warn('Failed to update fragment', error); + bugsnagInstance.notify(error); throw error; } -} +}; /** * Deletes user saved fragment using specified fragment id @@ -386,26 +380,24 @@ export const updateFragment = async (fragmentId: string, title: string, body: st export const deleteFragment = async (fragmentId: string) => { try { const data = { id: fragmentId }; - const response = await ecencyApi.post(`/private-api/fragments-delete`, data); + const response = await ecencyApi.post('/private-api/fragments-delete', data); return response.data; } catch (error) { - console.warn("Failed to delete fragment", error); - bugsnagInstance.notify(error) + console.warn('Failed to delete fragment', error); + bugsnagInstance.notify(error); throw error; } -} +}; - - -/** -* ************************************ -* ACTIVITES ECENCY APIS IMPLEMENTATION -* ************************************ -*/ +/** + * ************************************ + * ACTIVITES ECENCY APIS IMPLEMENTATION + * ************************************ + */ export const getLeaderboard = async (duration: 'day' | 'week' | 'month') => { try { - const response = await ecencyApi.get(`private-api/leaderboard/${duration}`) + const response = await ecencyApi.get(`private-api/leaderboard/${duration}`); const rawData = response.data; if (!rawData || !isArray(rawData)) { @@ -413,10 +405,10 @@ export const getLeaderboard = async (duration: 'day' | 'week' | 'month') => { } return rawData; } catch (error) { - bugsnagInstance.notify(error) + bugsnagInstance.notify(error); throw error; } -} +}; /** * fetches notifications from ecency server using filter and since props @@ -424,89 +416,85 @@ export const getLeaderboard = async (duration: 'day' | 'week' | 'month') => { * @returns array of notifications */ export const getNotifications = async (data: { - filter?: "rvotes" | "mentions" | "follows" | "replies" | "reblogs" | "transfers" | "delegations", - since?: string + filter?: 'rvotes' | 'mentions' | 'follows' | 'replies' | 'reblogs' | 'transfers' | 'delegations'; + since?: string; }) => { try { - const response = await ecencyApi.post(`/private-api/notifications`, data); + const response = await ecencyApi.post('/private-api/notifications', data); return response.data; } catch (error) { - console.warn("Failed to get notifications", error) - bugsnagInstance.notify(error) + console.warn('Failed to get notifications', error); + bugsnagInstance.notify(error); throw error; } -} - +}; export const getUnreadNotificationCount = async (accessToken?: string) => { try { - const data = accessToken ? { code: accessToken } : {} - const response = await ecencyApi.post(`/private-api/notifications/unread`, data) + const data = accessToken ? { code: accessToken } : {}; + const response = await ecencyApi.post('/private-api/notifications/unread', data); return response.data ? response.data.count : 0; } catch (error) { bugsnagInstance.notify(error); return 0; } -} +}; export const markNotifications = async (id: string | null = null) => { try { const data = id ? { id } : {}; - const response = await ecencyApi.post((`/private-api/notifications/mark`), data); - return response.data + const response = await ecencyApi.post('/private-api/notifications/mark', data); + return response.data; } catch (error) { bugsnagInstance.notify(error); - throw error + throw error; } }; - export const setPushToken = async (data, accessToken = null) => { try { if (!data.username) { - console.log("skipping push token setting, as no user is provided") + console.log('skipping push token setting, as no user is provided'); return; } - if(accessToken){ - data.code = accessToken + if (accessToken) { + data.code = accessToken; } - const res = await await ecencyApi.post((`/private-api/register-device`), data); + const res = await await ecencyApi.post('/private-api/register-device', data); return res.data; - } catch (error) { - console.warn("Failed to set push token on server") + console.warn('Failed to set push token on server'); bugsnagInstance.notify(error); } -} +}; -/** +/** * ************************************ - * SEARCH ECENCY APIS IMPLEMENTATION + * SEARCH ECENCY APIS IMPLEMENTATION * ************************************ */ export const search = async (data: { - q: string, - sort: string, - hideLow: string, - since?: string, - scroll_id?: string + q: string; + sort: string; + hideLow: string; + since?: string; + scroll_id?: string; }) => { try { const response = await ecencyApi.post('/search-api/search', data); return response.data; } catch (error) { - console.warn("Search failed", error); + console.warn('Search failed', error); bugsnagInstance.notify(error); throw error; } -} - +}; /** - * + * * @param q query * @returns array of path strings */ @@ -516,15 +504,14 @@ export const searchPath = async (q: string) => { const response = await ecencyApi.post('/search-api/search-path', data); return response.data; } catch (error) { - console.warn("path search failed", error) + console.warn('path search failed', error); bugsnagInstance.notify(error); - throw error + throw error; } -} - +}; /** - * + * * @param q query * @param limit number of posts to fetch * @param random random @@ -536,19 +523,18 @@ export const searchAccount = async (q: string = '', limit: number = 20, random: q, limit, random, - } - const response = await ecencyApi.post(`/search-api/search-account`, data) + }; + const response = await ecencyApi.post('/search-api/search-account', data); return response.data; } catch (error) { - console.warn("account search failed", error) + console.warn('account search failed', error); bugsnagInstance.notify(error); throw error; } -} - +}; /** - * + * * @param q query * @param limit number of posts to fetch * @param random random @@ -560,32 +546,30 @@ export const searchTag = async (q: string = '', limit: number = 20, random: numb q, limit, random, - } - const response = await ecencyApi.post(`/search-api/search-tag`, data); + }; + const response = await ecencyApi.post('/search-api/search-tag', data); return response.data; } catch (error) { - console.warn("tag search failed", error) + console.warn('tag search failed', error); bugsnagInstance.notify(error); - throw error + throw error; } -} +}; - - -/** +/** * ************************************ - * SCHEDULES ECENCY APIS IMPLEMENTATION + * SCHEDULES ECENCY APIS IMPLEMENTATION * ************************************ */ /** * Adds new post to scheduled posts - * @param permlink - * @param title - * @param body - * @param meta - * @param options - * @param scheduleDate + * @param permlink + * @param title + * @param body + * @param meta + * @param options + * @param scheduleDate * @returns All scheduled posts */ export const addSchedule = async ( @@ -594,7 +578,7 @@ export const addSchedule = async ( body: string, meta: any, options: any, - scheduleDate: string + scheduleDate: string, ) => { try { const data = { @@ -605,16 +589,15 @@ export const addSchedule = async ( schedule: scheduleDate, options, reblog: 0, - } - const response = await ecencyApi - .post('/private-api/schedules-add', data) + }; + const response = await ecencyApi.post('/private-api/schedules-add', data); return response.data; } catch (error) { - console.warn("Failed to add post to schedule", error) + console.warn('Failed to add post to schedule', error); bugsnagInstance.notify(error); throw error; } -} +}; /** * Fetches all scheduled posts against current user @@ -622,90 +605,89 @@ export const addSchedule = async ( */ export const getSchedules = async () => { try { - const response = await ecencyApi.post(`/private-api/schedules`) - return response.data; + const response = await ecencyApi.post('/private-api/schedules'); + return response.data || []; } catch (error) { - console.warn("Failed to get schedules") - bugsnagInstance.notify(error) + console.warn('Failed to get schedules'); + bugsnagInstance.notify(error); throw error; } -} +}; /** * Removes post from scheduled posts using post id; - * @param id + * @param id * @returns array of scheduled posts */ export const deleteScheduledPost = async (id: string) => { try { const data = { id }; - const response = await ecencyApi.post(`/private-api/schedules-delete`, data); - return response; + const response = await ecencyApi.post('/private-api/schedules-delete', data); + return response.data || []; } catch (error) { - console.warn("Failed to delete scheduled post") - bugsnagInstance.notify(error) + console.warn('Failed to delete scheduled post'); + bugsnagInstance.notify(error); throw error; } -} +}; /** * Moves scheduled post to draft using schedule id - * @param id + * @param id * @returns Array of scheduled posts */ export const moveScheduledToDraft = async (id: string) => { try { - const data = { id } - const response = await ecencyApi.post(`/private-api/schedules-move`, data); + const data = { id }; + const response = await ecencyApi.post('/private-api/schedules-move', data); return response.data; } catch (error) { - console.warn("Failed to move scheduled post to drafts") - bugsnagInstance.notify(error) + console.warn('Failed to move scheduled post to drafts'); + bugsnagInstance.notify(error); throw error; } -} +}; // Old image service -/** +/** * ************************************ - * IMAGES ECENCY APIS IMPLEMENTATION + * IMAGES ECENCY APIS IMPLEMENTATION * ************************************ */ - export const getImages = async () => { try { - const response = await ecencyApi.post('/private-api/images') + const response = await ecencyApi.post('/private-api/images'); return response.data; } catch (error) { console.warn('Failed to get images', error); bugsnagInstance.notify(error); } -} +}; export const addImage = async (url: string) => { try { const data = { url }; - const response = await ecencyApi.post(`/private-api/images-add`, data); + const response = await ecencyApi.post('/private-api/images-add', data); return response.data; } catch (error) { console.warn('Failed to add image', error); bugsnagInstance.notify(error); throw error; } -} +}; export const deleteImage = async (id: string) => { try { const data = { id }; - const response = await ecencyApi.post(`/private-api/images-delete`, data); + const response = await ecencyApi.post('/private-api/images-delete', data); return response.data; } catch (error) { console.warn('Failed to delete image', error); bugsnagInstance.notify(error); throw error; } -} +}; export const uploadImage = async (media, username, sign, uploadProgress = null) => { try { @@ -718,16 +700,15 @@ export const uploadImage = async (media, username, sign, uploadProgress = null) const fData = new FormData(); fData.append('file', file); - - const res = await upload(fData, username, sign, uploadProgress) + + const res = await upload(fData, username, sign, uploadProgress); if (!res || !res.data) { - throw new Error("Returning response missing media data"); + throw new Error('Returning response missing media data'); } return res; - } catch (error) { - console.warn("Image upload failed", error) - return { error } + console.warn('Image upload failed', error); + return { error }; } }; @@ -735,8 +716,6 @@ export const uploadImage = async (media, username, sign, uploadProgress = null) export const getNodes = () => serverList.get().then((resp) => resp.data.hived || SERVER_LIST); - - /** * refreshes access token using refresh token * @param code refresh token @@ -746,16 +725,14 @@ export const getSCAccessToken = async (code: string) => { try { const response = await ecencyApi.post('/auth-api/hs-token-refresh', { code, - }) + }); return response.data; } catch (error) { - console.warn("failed to refresh token") + console.warn('failed to refresh token'); bugsnagInstance.notify(error); - throw error + throw error; } -} - - +}; /** * fetches promoted posts for tab content @@ -771,31 +748,26 @@ export const getPromotedEntries = async (username: string) => { ); }); } catch (error) { - console.warn("Failed to get promoted enties") + console.warn('Failed to get promoted enties'); bugsnagInstance.notify(error); return error; } }; - - export const purchaseOrder = (data) => api .post('/purchase-order', data) .then((resp) => resp.data) .catch((error) => bugsnagInstance.notify(error)); - - export const getPostReblogs = (data) => api .get(`/post-reblogs/${data.author}/${data.permlink}`) .then((resp) => resp.data) .catch((error) => bugsnagInstance.notify(error)); - /** - * Registers new user with ecency and hive, on confirmation sends + * Registers new user with ecency and hive, on confirmation sends * details to user email * @param username for new user * @param email of new user @@ -807,8 +779,8 @@ export const signUp = async (username: string, email: string, referral?: string) const data = { username, email, - referral - } + referral, + }; const response = await ecencyApi.post('/private-api/account-create', data); return response.status === 202; } catch (error) { @@ -817,31 +789,35 @@ export const signUp = async (username: string, email: string, referral?: string) } }; -/** +/** * ************************************ - * REFERRAL API IMPLEMENTATION + * REFERRAL API IMPLEMENTATION * ************************************ */ -export const getReferralsList = async (username: string, maxId: number | undefined): Promise => { +export const getReferralsList = async ( + username: string, + maxId: number | undefined, +): Promise => { try { const res = await ecencyApi.get(`/private-api/referrals/${username}`, { params: { - max_id: maxId - } + max_id: maxId, + }, }); console.log('Referrals List', username, res.data); if (!res.data) { throw new Error('No Referrals for this user!'); } - const referralsList = res.data.length > 0 ? res.data.map((referralItem: any) => convertReferral(referralItem)) : []; + const referralsList = + res.data.length > 0 ? res.data.map((referralItem: any) => convertReferral(referralItem)) : []; return referralsList; } catch (error) { bugsnagInstance.notify(error); console.warn(error); throw error; } -} +}; export const getReferralsStats = async (username: string): Promise => { try { @@ -856,20 +832,23 @@ export const getReferralsStats = async (username: string): Promise console.warn(error); throw error; } -} +}; -/** +/** * ************************************ - * EDIT HISTORY API IMPLEMENTATION + * EDIT HISTORY API IMPLEMENTATION * ************************************ */ -export const getCommentHistory = async (author: string, permlink: string): Promise => { +export const getCommentHistory = async ( + author: string, + permlink: string, +): Promise => { try { const data = { author, - permlink - } + permlink, + }; const res = await ecencyApi.post('/private-api/comment-history', data); console.log('comment history', res.data); if (!res.data) { @@ -880,5 +859,4 @@ export const getCommentHistory = async (author: string, permlink: string): Promi bugsnagInstance.notify(error); throw error; } -} - +}; diff --git a/src/providers/queries/draftQueries.ts b/src/providers/queries/draftQueries.ts new file mode 100644 index 000000000..2ef560a1c --- /dev/null +++ b/src/providers/queries/draftQueries.ts @@ -0,0 +1,106 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useIntl } from 'react-intl'; +import { useAppDispatch } from '../../hooks'; +import { toastNotification } from '../../redux/actions/uiAction'; +import { + deleteDraft, + deleteScheduledPost, + getDrafts, + getSchedules, + moveScheduledToDraft, +} from '../ecency/ecency'; +import QUERIES from './queryKeys'; + +/** hook used to return user drafts */ +export const useGetDraftsQuery = () => { + return useQuery([QUERIES.DRAFTS.GET], _getDrafts); +}; + +/** used to return user schedules */ +export const useGetSchedulesQuery = () => { + return useQuery([QUERIES.SCHEDULES.GET], _getSchedules); +}; + +export const useDraftDeleteMutation = () => { + const queryClient = useQueryClient(); + const dispatch = useAppDispatch(); + const intl = useIntl(); + return useMutation(deleteDraft, { + retry: 3, + onSuccess: (data) => { + console.log('Success draft delete', data); + queryClient.setQueryData([QUERIES.DRAFTS.GET], _sortData(data)); + }, + onError: () => { + dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' }))); + }, + }); +}; + +export const useScheduleDeleteMutation = () => { + const queryClient = useQueryClient(); + const dispatch = useAppDispatch(); + const intl = useIntl(); + return useMutation(deleteScheduledPost, { + retry: 3, + onSuccess: (data) => { + console.log('Success scheduled post delete', data); + queryClient.setQueryData([QUERIES.SCHEDULES.GET], _sortData(data)); + }, + onError: () => { + dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' }))); + }, + }); +}; + +export const useMoveScheduleToDraftsMutation = () => { + const queryClient = useQueryClient(); + const dispatch = useAppDispatch(); + const intl = useIntl(); + return useMutation(moveScheduledToDraft, { + retry: 3, + onSuccess: (data) => { + console.log('Moved to drafts data', data); + queryClient.setQueryData([QUERIES.SCHEDULES.GET], _sortData(data)); + queryClient.invalidateQueries([QUERIES.DRAFTS.GET]); + dispatch(toastNotification(intl.formatMessage({ id: 'alert.success_moved' }))); + }, + onError: () => { + dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' }))); + }, + }); +}; + +const _getDrafts = async () => { + try { + const data = await getDrafts(); + return _sortData(data || []); + } catch (err) { + throw new Error('draft.load_error'); + } +}; + +const _getSchedules = async () => { + try { + const data = await getSchedules(); + return _sortDataS(data); + } catch (err) { + throw new Error('drafts.load_error'); + } +}; + +const _sortDataS = (data) => + data.sort((a, b) => { + const dateA = new Date(a.schedule).getTime(); + const dateB = new Date(b.schedule).getTime(); + + return dateB > dateA ? 1 : -1; + }); + +const _sortData = (data) => + data.sort((a, b) => { + const dateA = new Date(a.created).getTime(); + const dateB = new Date(b.created).getTime(); + + return dateB > dateA ? 1 : -1; + }); diff --git a/src/providers/queries/index.ts b/src/providers/queries/index.ts new file mode 100644 index 000000000..9a9705790 --- /dev/null +++ b/src/providers/queries/index.ts @@ -0,0 +1,24 @@ +import { QueryClient } from '@tanstack/react-query'; +import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'; +import AsyncStorage from '@react-native-community/async-storage'; +import { PersistQueryClientProviderProps } from '@tanstack/react-query-persist-client'; + +export const initQueryClient = () => { + const asyncStoragePersister = createAsyncStoragePersister({ + storage: AsyncStorage, + }); + + const client = new QueryClient({ + //Query client configurations go here... + defaultOptions: { + queries: { + cacheTime: 1000 * 60 * 60 * 24, // 24 hours + }, + }, + }); + + return { + client, + persistOptions: { persister: asyncStoragePersister }, + } as PersistQueryClientProviderProps; +}; diff --git a/src/providers/queries/queryKeys.ts b/src/providers/queries/queryKeys.ts new file mode 100644 index 000000000..f0195924d --- /dev/null +++ b/src/providers/queries/queryKeys.ts @@ -0,0 +1,10 @@ +const QUERIES = { + DRAFTS: { + GET: 'QUERY_GET_DRAFTS', + }, + SCHEDULES: { + GET: 'QUERY_GET_SCHEDULES', + }, +}; + +export default QUERIES; diff --git a/src/screens/drafts/container/draftsContainer.js b/src/screens/drafts/container/draftsContainer.js deleted file mode 100644 index 55034bfc3..000000000 --- a/src/screens/drafts/container/draftsContainer.js +++ /dev/null @@ -1,159 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { connect } from 'react-redux'; -import { Alert } from 'react-native'; -import { injectIntl } from 'react-intl'; - -// Services and Actions -import { - getDrafts, - deleteDraft, - getSchedules, - moveScheduledToDraft, - deleteScheduledPost, -} from '../../../providers/ecency/ecency'; -import { toastNotification } from '../../../redux/actions/uiAction'; - -// Middleware - -// Constants -import { default as ROUTES } from '../../../constants/routeNames'; - -// Utilities - -// Component -import DraftsScreen from '../screen/draftsScreen'; - -const DraftsContainer = ({ currentAccount, intl, navigation, dispatch, route }) => { - const [drafts, setDrafts] = useState([]); - const [schedules, setSchedules] = useState([]); - const [isLoading, setIsLoading] = useState(false); - - const [initialTabIndex] = useState(route.params?.showSchedules ? 1 : 0); - - useEffect(() => { - _getDrafts(); - _getSchedules(); - }, []); - - // Component Functions - - const _getSchedules = () => { - setIsLoading(true); - - getSchedules() - .then((data) => { - setSchedules(_sortDataS(data)); - setIsLoading(false); - }) - .catch(() => { - Alert.alert(intl.formatMessage({ id: 'drafts.load_error' })); - setIsLoading(false); - }); - }; - - const _getDrafts = () => { - setIsLoading(true); - - getDrafts() - .then((data) => { - setDrafts(_sortData(data)); - setIsLoading(false); - }) - .catch(() => { - Alert.alert(intl.formatMessage({ id: 'drafts.load_error' })); - setIsLoading(false); - }); - }; - - const _removeDraft = (id) => { - deleteDraft(id) - .then(() => { - const newDrafts = [...drafts].filter((draft) => draft._id !== id); - setDrafts(_sortData(newDrafts)); - }) - .catch(() => { - Alert.alert(intl.formatMessage({ id: 'alert.fail' })); - }); - }; - - const _removeSchedule = (id) => { - deleteScheduledPost(id) - .then((res) => { - const newSchedules = [...schedules].filter((schedule) => schedule._id !== id); - - setSchedules(_sortDataS(newSchedules)); - }) - .catch(() => { - Alert.alert(intl.formatMessage({ id: 'alert.fail' })); - }); - }; - - const _moveScheduleToDraft = (id) => { - moveScheduledToDraft(id) - .then((res) => { - dispatch( - toastNotification( - intl.formatMessage({ - id: 'alert.success_moved', - }), - ), - ); - - _getDrafts(); - _getSchedules(); - }) - .catch((error) => { - console.warn('Failed to move scheduled post to drafts'); - dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' }))); - }); - }; - - const _editDraft = (id) => { - const selectedDraft = drafts.find((draft) => draft._id === id); - - navigation.navigate({ - name: ROUTES.SCREENS.EDITOR, - key: `editor_draft_${id}`, - params: { - draft: selectedDraft, - fetchPost: _getDrafts, - }, - }); - }; - - const _sortData = (data) => - data.sort((a, b) => { - const dateA = new Date(a.created).getTime(); - const dateB = new Date(b.created).getTime(); - - return dateB > dateA ? 1 : -1; - }); - - const _sortDataS = (data) => - data.sort((a, b) => { - const dateA = new Date(a.schedule).getTime(); - const dateB = new Date(b.schedule).getTime(); - - return dateB > dateA ? 1 : -1; - }); - - return ( - - ); -}; - -const mapStateToProps = (state) => ({ - currentAccount: state.account.currentAccount, -}); - -export default injectIntl(connect(mapStateToProps)(DraftsContainer)); diff --git a/src/screens/drafts/container/draftsContainer.tsx b/src/screens/drafts/container/draftsContainer.tsx new file mode 100644 index 000000000..044ef9270 --- /dev/null +++ b/src/screens/drafts/container/draftsContainer.tsx @@ -0,0 +1,93 @@ +import React, { useState } from 'react'; +import { connect } from 'react-redux'; +import { injectIntl } from 'react-intl'; + +// Services and Actions +import { + useDraftDeleteMutation, + useGetDraftsQuery, + useGetSchedulesQuery, + useMoveScheduleToDraftsMutation, + useScheduleDeleteMutation, +} from '../../../providers/queries/draftQueries'; + +// Middleware + +// Constants +import { default as ROUTES } from '../../../constants/routeNames'; + +// Utilities + +// Component +import DraftsScreen from '../screen/draftsScreen'; + +const DraftsContainer = ({ currentAccount, navigation, route }) => { + const { mutate: deleteDraft, isLoading: isDeletingDraft } = useDraftDeleteMutation(); + const { mutate: deleteSchedule, isLoading: isDeletingSchedule } = useScheduleDeleteMutation(); + const { + mutate: moveScheduleToDrafts, + isLoading: isMovingToDrafts, + } = useMoveScheduleToDraftsMutation(); + + const { + isLoading: isLoadingDrafts, + data: drafts = [], + isFetching: isFetchingDrafts, + refetch: refetchDrafts, + } = useGetDraftsQuery(); + + const { + isLoading: isLoadingSchedules, + data: schedules = [], + isFetching: isFetchingSchedules, + refetch: refetchSchedules, + } = useGetSchedulesQuery(); + + const [initialTabIndex] = useState(route.params?.showSchedules ? 1 : 0); + + // Component Functions + const _onRefresh = () => { + refetchDrafts(); + refetchSchedules(); + }; + + const _editDraft = (id: string) => { + const selectedDraft = drafts.find((draft) => draft._id === id); + + navigation.navigate({ + name: ROUTES.SCREENS.EDITOR, + key: `editor_draft_${id}`, + params: { + draft: selectedDraft, + fetchPost: refetchDrafts, + }, + }); + }; + + const _isLoading = + isLoadingDrafts || isLoadingSchedules || isFetchingDrafts || isFetchingSchedules; + + const _isDeleting = isDeletingDraft || isDeletingSchedule || isMovingToDrafts; + + return ( + + ); +}; + +const mapStateToProps = (state) => ({ + currentAccount: state.account.currentAccount, +}); + +export default injectIntl(connect(mapStateToProps)(DraftsContainer)); diff --git a/src/screens/drafts/screen/draftsScreen.js b/src/screens/drafts/screen/draftsScreen.js index 9ed00e8a5..8029a94b0 100644 --- a/src/screens/drafts/screen/draftsScreen.js +++ b/src/screens/drafts/screen/draftsScreen.js @@ -1,6 +1,6 @@ -import React, { useState, useRef } from 'react'; +import React from 'react'; import { injectIntl } from 'react-intl'; -import { View, FlatList, Text, Platform } from 'react-native'; +import { View, FlatList, Text, Platform, RefreshControl } from 'react-native'; import ScrollableTabView from 'react-native-scrollable-tab-view'; // Utils @@ -14,7 +14,7 @@ import { BasicHeader, TabBar, DraftListItem, PostCardPlaceHolder } from '../../. // Styles import globalStyles from '../../../globalStyles'; import styles from './draftStyles'; -import { OptionsModal } from '../../../components/atoms'; +import { useAppSelector } from '../../../hooks'; const DraftsScreen = ({ currentAccount, @@ -22,20 +22,15 @@ const DraftsScreen = ({ editDraft, removeSchedule, isLoading, + isDeleting, + onRefresh, intl, drafts, schedules, moveScheduleToDraft, initialTabIndex, }) => { - const [selectedId, setSelectedId] = useState(null); - const ActionSheetRef = useRef(null); - - const _onActionPress = (index) => { - if (index === 0) { - moveScheduleToDraft(selectedId); - } - }; + const isDarkTheme = useAppSelector((state) => state.application.isDarkTheme); // Component Functions const _renderItem = (item, type) => { @@ -53,12 +48,7 @@ const DraftsScreen = ({ const isSchedules = type === 'schedules'; const _onItemPress = () => { - if (isSchedules) { - setSelectedId(item._id); - if (ActionSheetRef.current) { - ActionSheetRef.current.show(); - } - } else { + if (!isSchedules) { editDraft(item._id); } }; @@ -75,11 +65,13 @@ const DraftsScreen = ({ username={currentAccount.name} reputation={currentAccount.reputation} handleOnPressItem={_onItemPress} + handleOnMovePress={moveScheduleToDraft} handleOnRemoveItem={isSchedules ? removeSchedule : removeDraft} id={item._id} key={item._id} status={item.status} isSchedules={isSchedules} + isDeleting={isDeleting} /> ); }; @@ -111,6 +103,16 @@ const DraftsScreen = ({ removeClippedSubviews={false} renderItem={({ item }) => _renderItem(item, type)} ListEmptyComponent={_renderEmptyContent()} + refreshControl={ + + } /> ); @@ -152,22 +154,6 @@ const DraftsScreen = ({ {_getTabItem(schedules, 'schedules')} - ); }; diff --git a/src/screens/editor/children/postOptionsModal.tsx b/src/screens/editor/children/postOptionsModal.tsx index cbe938621..29896b650 100644 --- a/src/screens/editor/children/postOptionsModal.tsx +++ b/src/screens/editor/children/postOptionsModal.tsx @@ -2,63 +2,70 @@ import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'rea import { useIntl } from 'react-intl'; import { View } from 'react-native'; -import { BeneficiarySelectionContent, DateTimePicker, MainButton, Modal, SettingsItem } from '../../../components'; +import { View as AnimatedView } from 'react-native-animatable'; +import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; +import { + BeneficiarySelectionContent, + DateTimePicker, + MainButton, + Modal, + SettingsItem, +} from '../../../components'; import styles from './postOptionsModalStyles'; import ThumbSelectionContent from './thumbSelectionContent'; -import {View as AnimatedView} from 'react-native-animatable'; -import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; const REWARD_TYPES = [ { - key:'default', - intlId:'editor.reward_default' + key: 'default', + intlId: 'editor.reward_default', }, { - key:'sp', - intlId:'editor.reward_power_up' + key: 'sp', + intlId: 'editor.reward_power_up', }, { - key:'dp', - intlId:'editor.reward_decline' + key: 'dp', + intlId: 'editor.reward_decline', }, -] - - +]; export interface PostOptionsModalRef { - showModal:()=>void; + showModal: () => void; } - interface PostOptionsModalProps { - body:string; - draftId:string; - thumbIndex:number, - isEdit:boolean; - isCommunityPost:boolean; + body: string; + draftId: string; + thumbUrl: string; + isEdit: boolean; + isCommunityPost: boolean; rewardType: string; isUploading: boolean; - handleRewardChange:(rewardType:string)=>void; - handleThumbSelection:(index:number)=>void; - handleScheduleChange:(datetime:string|null)=>void; - handleShouldReblogChange:(shouldReblog:boolean)=>void; - handleFormUpdate:()=>void; + handleRewardChange: (rewardType: string) => void; + handleThumbSelection: (url: string) => void; + handleScheduleChange: (datetime: string | null) => void; + handleShouldReblogChange: (shouldReblog: boolean) => void; + handleFormUpdate: () => void; } -const PostOptionsModal = forwardRef(({ - body, - draftId, - thumbIndex, - isEdit, - isCommunityPost, - rewardType, - isUploading, - handleRewardChange, - handleThumbSelection, - handleScheduleChange, - handleShouldReblogChange, - handleFormUpdate -}: PostOptionsModalProps, ref) => { +const PostOptionsModal = forwardRef( + ( + { + body, + draftId, + thumbUrl, + isEdit, + isCommunityPost, + rewardType, + isUploading, + handleRewardChange, + handleThumbSelection, + handleScheduleChange, + handleShouldReblogChange, + handleFormUpdate, + }: PostOptionsModalProps, + ref, + ) => { const intl = useIntl(); const [showModal, setShowModal] = useState(false); @@ -70,84 +77,83 @@ const PostOptionsModal = forwardRef(({ // removed the useeffect causing index reset bug - useEffect(()=>{ - if(!scheduleLater){ - handleScheduleChange(null) - }else if(scheduledFor) { - handleScheduleChange(scheduledFor) - } - }, [scheduleLater, scheduledFor]) - useEffect(() => { - handleShouldReblogChange(shouldReblog) - }, [shouldReblog]) + if (!scheduleLater) { + handleScheduleChange(null); + } else if (scheduledFor) { + handleScheduleChange(scheduledFor); + } + }, [scheduleLater, scheduledFor]); useEffect(() => { - if(!isCommunityPost && shouldReblog){ + handleShouldReblogChange(shouldReblog); + }, [shouldReblog]); + + useEffect(() => { + if (!isCommunityPost && shouldReblog) { setShouldReblog(false); } - }, [isCommunityPost]) + }, [isCommunityPost]); // load rewardtype from props if it is already saved in drafts useEffect(() => { - if(rewardType){ - let rewardTypeKey = REWARD_TYPES.findIndex((item) => item.key === rewardType) + if (rewardType) { + let rewardTypeKey = REWARD_TYPES.findIndex((item) => item.key === rewardType); setRewardTypeIndex(rewardTypeKey); } - },[rewardType]) + }, [rewardType]); useImperativeHandle(ref, () => ({ - show: () => { - setShowModal(true); - }, - })); + show: () => { + setShowModal(true); + }, + })); - - const _handleRewardChange = (index:number) => { - setRewardTypeIndex(index) - const rewardTypeKey = REWARD_TYPES[index].key + const _handleRewardChange = (index: number) => { + setRewardTypeIndex(index); + const rewardTypeKey = REWARD_TYPES[index].key; if (handleRewardChange) { handleRewardChange(rewardTypeKey); } - } + }; - const _handleDatePickerChange = (date:string) => { + const _handleDatePickerChange = (date: string) => { setScheduledFor(date); - } + }; const _onDonePress = () => { setShowModal(false); handleFormUpdate(); - } - + }; + // handle index change here instead of useeffetc - const _handleThumbIndexSelection = (index:number) => { - handleThumbSelection(index) - } + const _handleThumbIndexSelection = (url: string) => { + handleThumbSelection(url); + }; const _renderContent = () => ( - + {!isEdit && ( <> { + handleOnChange={(index) => { setScheduleLater(index === 1); if (index !== 1) { handleScheduleChange(null); } }} /> - + {scheduleLater && ( - )} - + intl.formatMessage({ id: type.intlId})) - } + options={REWARD_TYPES.map((type) => intl.formatMessage({ id: type.intlId }))} selectedOptionIndex={rewardTypeIndex} handleOnChange={_handleRewardChange} /> - - + {isCommunityPost && ( )} - + )} - - - - + {!isEdit && ( - + )} - - + text={intl.formatMessage({ id: 'editor.done' })} + /> - - - ) + ); - - return ( - { setShowModal(false); @@ -228,14 +221,14 @@ const PostOptionsModal = forwardRef(({ isFullScreen isCloseButton presentationStyle="formSheet" - title={intl.formatMessage({id:"editor.settings_title"})} + title={intl.formatMessage({ id: 'editor.settings_title' })} animationType="slide" style={styles.modalStyle} > - {_renderContent()} - - - ); -}); + {_renderContent()} + + ); + }, +); -export default PostOptionsModal +export default PostOptionsModal; diff --git a/src/screens/editor/children/styles.ts b/src/screens/editor/children/styles.ts index f4393f277..ad7a2620c 100644 --- a/src/screens/editor/children/styles.ts +++ b/src/screens/editor/children/styles.ts @@ -1,59 +1,60 @@ -import { ViewStyle } from 'react-native'; 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', + }, + checkContainer: { + position: 'absolute', + top: 12, + left: 6, + backgroundColor: '$pureWhite', + borderRadius: 12, + }, + 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, }, - 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, - } }); diff --git a/src/screens/editor/children/thumbSelectionContent.tsx b/src/screens/editor/children/thumbSelectionContent.tsx index 870bbd553..332300435 100644 --- a/src/screens/editor/children/thumbSelectionContent.tsx +++ b/src/screens/editor/children/thumbSelectionContent.tsx @@ -1,87 +1,109 @@ import React, { useEffect, useState } from 'react'; import { useIntl } from 'react-intl'; -import { ActivityIndicator, Text, TouchableOpacity, View } from 'react-native'; +import { ActivityIndicator, Alert, Text, TouchableOpacity, View } from 'react-native'; import FastImage from 'react-native-fast-image'; import { FlatList } from 'react-native-gesture-handler'; +import ESStyleSheet from 'react-native-extended-stylesheet'; +import EStyleSheet from 'react-native-extended-stylesheet'; +import { View as AnimatedView } from 'react-native-animatable'; import { extractImageUrls } from '../../../utils/editor'; import styles from './styles'; -import ESStyleSheet from 'react-native-extended-stylesheet'; +import { Icon } from '../../../components'; interface ThumbSelectionContentProps { - body: string; - thumbIndex: number; - isUploading: boolean; - onThumbSelection: (index: number) => void; + body: string; + thumbUrl: string; + isUploading: boolean; + onThumbSelection: (url: string) => void; } -const ThumbSelectionContent = ({ body, thumbIndex, onThumbSelection, isUploading }: ThumbSelectionContentProps) => { - const intl = useIntl(); +const ThumbSelectionContent = ({ + body, + thumbUrl, + onThumbSelection, + isUploading, +}: ThumbSelectionContentProps) => { + const intl = useIntl(); - const [imageUrls, setImageUrls] = useState([]); - const [needMore, setNeedMore] = useState(true); + const [imageUrls, setImageUrls] = useState([]); + const [needMore, setNeedMore] = useState(true); + const [thumbIndex, setThumbIndex] = useState(0); - useEffect(() => { - const urls = extractImageUrls({ body }); + useEffect(() => { + const urls = extractImageUrls({ body }); - if (urls.length < 2) { - setNeedMore(true); - onThumbSelection(0); - setImageUrls([]) - } else { - setNeedMore(false); - setImageUrls(urls) - } - }, [body]) - - - //VIEW_RENDERERS - const _renderImageItem = ({ item, index }: { item: string, index: number }) => { - const _onPress = () => { - onThumbSelection(index); - } - - const selectedStyle = index === thumbIndex ? styles.selectedStyle : null - - return ( - _onPress()} > - - - ) + if (urls.length < 2) { + setNeedMore(true); + onThumbSelection(urls[0] || ''); + setThumbIndex(0); + setImageUrls([]); + } else { + setNeedMore(false); + setImageUrls(urls); } - const _renderHeader = () => ( - isUploading && - - - + const _urlIndex = urls.indexOf(thumbUrl); + if (_urlIndex < 0) { + onThumbSelection(urls[0] || ''); + setThumbIndex(0); + } else { + setThumbIndex(_urlIndex); + } + }, [body]); - ) + //VIEW_RENDERERS + const _renderImageItem = ({ item, index }: { item: string; index: number }) => { + const _onPress = () => { + onThumbSelection(item); + setThumbIndex(index); + }; + const isSelected = item === thumbUrl && index === thumbIndex; return ( - - {intl.formatMessage({ id: 'editor.select_thumb' })} - { - needMore ? ( - {intl.formatMessage({ id: 'editor.add_more_imgs' })} - ) : ( - `${item}-${index}`} - horizontal={true} - contentContainerStyle={styles.listContainer} - showsHorizontalScrollIndicator={false} /> - ) - } - - + _onPress()}> + + {isSelected && ( + + + + )} + ); + }; + + const _renderHeader = () => + isUploading && ( + + + + ); + + return ( + + {intl.formatMessage({ id: 'editor.select_thumb' })} + {needMore ? ( + + {intl.formatMessage({ id: 'editor.add_more_imgs' })} + + ) : ( + `${item}-${index}`} + horizontal={true} + contentContainerStyle={styles.listContainer} + showsHorizontalScrollIndicator={false} + /> + )} + + ); }; export default ThumbSelectionContent; diff --git a/src/screens/editor/children/thumbSelectionModal.tsx b/src/screens/editor/children/thumbSelectionModal.tsx index 0e23b8fdf..285090596 100644 --- a/src/screens/editor/children/thumbSelectionModal.tsx +++ b/src/screens/editor/children/thumbSelectionModal.tsx @@ -1,22 +1,20 @@ -import React, { useImperativeHandle, useRef, useState } from 'react'; +import React, { useImperativeHandle, useRef, useState, forwardRef } from 'react'; import { FlatList } from 'react-native-gesture-handler'; import ActionSheet from 'react-native-actions-sheet'; import EStyleSheet from 'react-native-extended-stylesheet'; -import styles from './styles'; -import { extractImageUrls } from '../../../utils/editor'; import FastImage from 'react-native-fast-image'; -import { forwardRef } from 'react'; + import { View, Text, Alert, TouchableOpacity } from 'react-native'; import { useIntl } from 'react-intl'; - +import { extractImageUrls } from '../../../utils/editor'; +import styles from './styles'; export interface ThumbSelectionModalProps { - thumbIndex:number; - onThumbSelection:(index:number)=>void; + thumbUrl: string; + onThumbSelection: (index: number) => void; } - -const ThumbSelectionModal = ({ onThumbSelection, thumbIndex }:ThumbSelectionModalProps, ref) => { +const ThumbSelectionModal = ({ onThumbSelection, thumbUrl }: ThumbSelectionModalProps, ref) => { const intl = useIntl(); const [imageUrls, setImageUrls] = useState([]); @@ -24,82 +22,74 @@ const ThumbSelectionModal = ({ onThumbSelection, thumbIndex }:ThumbSelectionModa //CALLBACK_METHODS useImperativeHandle(ref, () => ({ - show: (postBody:string) => { - console.log("Showing action modal") + show: (postBody: string) => { + console.log('Showing action modal'); - const urls = extractImageUrls({body:postBody}); + const urls = extractImageUrls({ body: postBody }); - if(urls.length < 2){ - console.log("Skipping modal show as post images are less than 2"); - Alert.alert( - intl.formatMessage({id:'editor.two_thumbs_required'}) - ) - onThumbSelection(0); - return; - } - - setImageUrls(urls); - sheetModalRef.current?.setModalVisible(true); + if (urls.length < 2) { + console.log('Skipping modal show as post images are less than 2'); + Alert.alert(intl.formatMessage({ id: 'editor.two_thumbs_required' })); + onThumbSelection(0); + return; } + + setImageUrls(urls); + sheetModalRef.current?.setModalVisible(true); + }, })); - - const _onSelection = (index:number) => { + const _onSelection = (index: number) => { onThumbSelection(index); sheetModalRef.current?.setModalVisible(false); - } - - + }; //VIEW_RENDERERS - const _renderImageItem = ({item, index}:{item:string, index:number}) => { - const _onPress = () => { - _onSelection(index); - } + const _renderImageItem = ({ item, index }: { item: string; index: number }) => { + const _onPress = () => { + _onSelection(index); + }; - const selectedStyle = index === thumbIndex ? styles.selectedStyle : null + const selectedStyle = item === thumbUrl ? styles.selectedStyle : null; return ( - _onPress()} > - - - ) - } - + _onPress()}> + + + ); + }; const _renderContent = () => { - return ( - - {intl.formatMessage({id:'editor.select_thumb'})} - `${item}-${index}`} - horizontal={true} - contentContainerStyle={styles.listContainer} - showsHorizontalScrollIndicator={false} - /> - - - ) - } - + return ( + + {intl.formatMessage({ id: 'editor.select_thumb' })} + `${item}-${index}`} + horizontal={true} + contentContainerStyle={styles.listContainer} + showsHorizontalScrollIndicator={false} + /> + + ); + }; return ( - - {_renderContent()} - + + {_renderContent()} + ); }; -export default forwardRef(ThumbSelectionModal); \ No newline at end of file +export default forwardRef(ThumbSelectionModal); diff --git a/src/screens/editor/container/editorContainer.tsx b/src/screens/editor/container/editorContainer.tsx index 9346c6676..e389284fb 100644 --- a/src/screens/editor/container/editorContainer.tsx +++ b/src/screens/editor/container/editorContainer.tsx @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { injectIntl } from 'react-intl'; -import { Alert } from 'react-native'; +import { Alert, AppState, AppStateStatus } from 'react-native'; import get from 'lodash/get'; import AsyncStorage from '@react-native-community/async-storage'; import { isArray } from 'lodash'; @@ -9,13 +9,8 @@ import { isArray } from 'lodash'; // Services and Actions import { Buffer } from 'buffer'; -import { - - addDraft, - updateDraft, - getDrafts, - addSchedule, -} from '../../../providers/ecency/ecency'; +import { useQueryClient } from '@tanstack/react-query'; +import { addDraft, updateDraft, getDrafts, addSchedule } from '../../../providers/ecency/ecency'; import { toastNotification, setRcOffer } from '../../../redux/actions/uiAction'; import { postContent, @@ -43,7 +38,13 @@ import { import EditorScreen from '../screen/editorScreen'; import { removeBeneficiaries, setBeneficiaries } from '../../../redux/actions/editorActions'; import { DEFAULT_USER_DRAFT_ID, TEMP_BENEFICIARIES_ID } from '../../../redux/constants/constants'; -import { deleteDraftCacheEntry, updateCommentCache, updateDraftCache } from '../../../redux/actions/cacheActions'; +import { + deleteDraftCacheEntry, + updateCommentCache, + updateDraftCache, +} from '../../../redux/actions/cacheActions'; +import QUERIES from '../../../providers/queries/queryKeys'; +import bugsnapInstance from '../../../config/bugsnag'; /* * Props Name Description Value @@ -54,6 +55,7 @@ import { deleteDraftCacheEntry, updateCommentCache, updateDraftCache } from '../ class EditorContainer extends Component { _isMounted = false; _updatedDraftFields = null; + _appState = AppState.currentState; constructor(props) { super(props); @@ -71,14 +73,12 @@ class EditorContainer extends Component { uploadProgress: 0, post: null, uploadedImage: null, - isDraft: false, community: [], rewardType: 'default', sharedSnippetText: null, onLoadDraftPress: false, - thumbIndex: 0, + thumbUrl: '', shouldReblog: false, - failedImageUploads: 0, }; } @@ -105,7 +105,6 @@ class EditorContainer extends Component { this.setState({ draftId: _draft._id, - isDraft: true, }); this._getStorageDraft(username, isReply, _draft); } @@ -124,8 +123,8 @@ class EditorContainer extends Component { if (navigationParams.isReply) { ({ isReply } = navigationParams); - if(post){ - draftId = `${currentAccount.name}/${post.author}/${post.permlink}` + if (post) { + draftId = `${currentAccount.name}/${post.author}/${post.permlink}`; } this.setState({ @@ -135,7 +134,7 @@ class EditorContainer extends Component { }); if (draftId) { this._getStorageDraft(username, isReply, { _id: draftId }); - } + } } if (navigationParams.isEdit) { @@ -155,12 +154,12 @@ class EditorContainer extends Component { } // handle file/text shared from ReceiveSharingIntent - if(hasSharedIntent){ + if (hasSharedIntent) { const files = navigationParams.files; console.log('files : ', files); - + files.forEach((el) => { - if (el.text) { + if (el.text) { this.setState({ sharedSnippetText: el.text, }); @@ -173,20 +172,34 @@ class EditorContainer extends Component { this._fetchDraftsForComparison(isReply); } this._requestKeyboardFocus(); + + AppState.addEventListener('change', this._handleAppStateChange); } - componentWillUnmount() { - this._isMounted = false; - } - - componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void { - if(prevState.rewardType !== this.state.rewardType || prevProps.beneficiariesMap !== this.props.beneficiariesMap){ + componentDidUpdate(prevProps: Readonly, prevState: Readonly): void { + if ( + prevState.rewardType !== this.state.rewardType || + prevProps.beneficiariesMap !== this.props.beneficiariesMap + ) { // update isDraftSaved when reward type or beneficiaries are changed in post options this._handleFormChanged(); } } + + componentWillUnmount() { + AppState.removeEventListener('change', this._handleAppStateChange); + this._isMounted = false; + } + + _handleAppStateChange = (nextAppState: AppStateStatus) => { + if (this._appState.match(/active|forground/) && nextAppState === 'inactive') { + this._saveCurrentDraft(this._updatedDraftFields); + } + this._appState = nextAppState; + }; + _getStorageDraft = async (username, isReply, paramDraft) => { - const { drafts, dispatch } = this.props; + const { drafts } = this.props; if (isReply) { const _draft = drafts.get(paramDraft._id); if (_draft && _draft.body) { @@ -204,15 +217,16 @@ class EditorContainer extends Component { //if _draft is returned and param draft is available, compare timestamp, use latest //if no draft, use result anayways - if (_localDraft && (!paramDraft || paramDraft.timestamp < _localDraft.updated)) { + const _remoteDraftModifiedAt = paramDraft ? new Date(paramDraft.modified).getTime() : 0; + const _useLocalDraft = _localDraft && _remoteDraftModifiedAt < _localDraft.updated; + if (_useLocalDraft) { this.setState({ draftPost: { body: get(_localDraft, 'body', ''), title: get(_localDraft, 'title', ''), tags: get(_localDraft, 'tags', '').split(','), - isDraft: paramDraft ? true : false, draftId: paramDraft ? paramDraft._id : null, - meta: _localDraft.meta ? _localDraft.meta : null + meta: _localDraft.meta ? _localDraft.meta : null, }, }); this._loadMeta(_localDraft); //load meta from local draft @@ -229,15 +243,13 @@ class EditorContainer extends Component { title: paramDraft.title, body: paramDraft.body, tags: _tags, - meta: paramDraft.meta ? paramDraft.meta : null + meta: paramDraft.meta ? paramDraft.meta : null, }, - isDraft: true, draftId: paramDraft._id, }); this._loadMeta(paramDraft); //load meta from param draft } - } }; @@ -248,9 +260,8 @@ class EditorContainer extends Component { const body = draft.body; if (draft.meta && draft.meta.image) { const urls = extractImageUrls({ body }); - const draftThumbIndex = urls.indexOf(draft.meta.image[0]); this.setState({ - thumbIndex: draftThumbIndex, + thumbUrl: draft.meta.image[0], }); } @@ -262,12 +273,14 @@ class EditorContainer extends Component { } if (draft._id && draft.meta && draft.meta.beneficiaries) { - if(isArray(draft.meta.beneficiaries)){ - const filteredBeneficiaries = draft.meta.beneficiaries.filter((item) => item.account !== currentAccount.username); //remove default beneficiary from array while saving + if (isArray(draft.meta.beneficiaries)) { + const filteredBeneficiaries = draft.meta.beneficiaries.filter( + (item) => item.account !== currentAccount.username, + ); //remove default beneficiary from array while saving dispatch(setBeneficiaries(draft._id || TEMP_BENEFICIARIES_ID, filteredBeneficiaries)); } } - } + }; _requestKeyboardFocus = () => { //50 ms timeout is added to avoid keyboard not showing up on android setTimeout(() => { @@ -286,7 +299,7 @@ class EditorContainer extends Component { * @param isReply **/ _fetchDraftsForComparison = async (isReply) => { - const { currentAccount, isLoggedIn, intl, dispatch, drafts } = this.props; + const { currentAccount, isLoggedIn, drafts } = this.props; const username = get(currentAccount, 'name', ''); //initilizes editor with reply or non remote id less draft @@ -311,8 +324,8 @@ class EditorContainer extends Component { } const remoteDrafts = await getDrafts(username); - - const idLessDraft = drafts.get(DEFAULT_USER_DRAFT_ID + username) + + const idLessDraft = drafts.get(DEFAULT_USER_DRAFT_ID + username); const loadRecentDraft = () => { //if no draft available means local draft is recent @@ -341,7 +354,6 @@ class EditorContainer extends Component { //initilize editor as draft this.setState({ draftId: _draft._id, - isDraft: true, }); this._getStorageDraft(username, isReply, _draft); }; @@ -361,21 +373,28 @@ class EditorContainer extends Component { const { draftId } = this.state; const { beneficiariesMap, currentAccount } = this.props; - return beneficiariesMap[draftId || TEMP_BENEFICIARIES_ID] - || [{ account: currentAccount.name, weight: 10000 }]; - } - + return ( + beneficiariesMap[draftId || TEMP_BENEFICIARIES_ID] || [ + { account: currentAccount.name, weight: 10000 }, + ] + ); + }; _saveDraftToDB = async (fields, saveAsNew = false) => { - const { isDraftSaved, draftId, thumbIndex, isReply, rewardType } = this.state; - const { currentAccount, dispatch, intl } = this.props; + const { isDraftSaved, draftId, thumbUrl, isReply, rewardType } = this.state; + const { currentAccount, dispatch, intl, queryClient } = this.props; - - if (isReply) { - this._saveCurrentDraft(this._updatedDraftFields) - return; + try { + //saves draft locallly + this._saveCurrentDraft(this._updatedDraftFields); + } catch (err) { + console.warn('local draft safe failed, skipping for remote only', err); + bugsnapInstance.notify(err); } + if (isReply) { + return; + } const beneficiaries = this._extractBeneficiaries(); @@ -395,14 +414,14 @@ class EditorContainer extends Component { tags: fields.tags.join(' '), }; } - - const meta = Object.assign({}, extractMetadata(draftField.body, thumbIndex), { + + const meta = Object.assign({}, extractMetadata(draftField.body, thumbUrl), { tags: draftField.tags, beneficiaries, - rewardType + rewardType, }); const jsonMeta = makeJsonMetadata(meta, draftField.tags); - + //update draft is draftId is present if (draftId && draftField && !saveAsNew) { await updateDraft(draftId, draftField.title, draftField.body, draftField.tags, jsonMeta); @@ -417,7 +436,12 @@ class EditorContainer extends Component { //create new darft otherwise else if (draftField) { - const response = await addDraft(draftField.title, draftField.body, draftField.tags, jsonMeta); + const response = await addDraft( + draftField.title, + draftField.body, + draftField.tags, + jsonMeta, + ); if (this._isMounted) { this.setState({ @@ -426,17 +450,18 @@ class EditorContainer extends Component { draftId: response._id, }); } - const filteredBeneficiaries = beneficiaries.filter((item) => item.account !== currentAccount.username); //remove default beneficiary from array while saving + const filteredBeneficiaries = beneficiaries.filter( + (item) => item.account !== currentAccount.username, + ); //remove default beneficiary from array while saving dispatch(setBeneficiaries(response._id, filteredBeneficiaries)); dispatch(removeBeneficiaries(TEMP_BENEFICIARIES_ID)); //clear local copy if darft save is successful const username = get(currentAccount, 'name', ''); - dispatch(deleteDraftCacheEntry(draftId || (DEFAULT_USER_DRAFT_ID + username))) + dispatch(deleteDraftCacheEntry(draftId || DEFAULT_USER_DRAFT_ID + username)); } - dispatch( toastNotification( intl.formatMessage({ @@ -445,9 +470,10 @@ class EditorContainer extends Component { ), ); - //call fetch post to drafts screen - this._navigationBackFetchDrafts(); + if (queryClient) { + queryClient.invalidateQueries([QUERIES.DRAFTS.GET]); + } } } catch (err) { console.warn('Failed to save draft to DB: ', err); @@ -456,9 +482,6 @@ class EditorContainer extends Component { isDraftSaving: false, isDraftSaved: false, }); - - //saves draft locally if remote draft save fails - this._saveCurrentDraft(this._updatedDraftFields) } dispatch( @@ -471,11 +494,9 @@ class EditorContainer extends Component { } }; - _updateDraftFields = (fields) => { - this._updatedDraftFields = fields; - } - + this._updatedDraftFields = fields; + }; _saveCurrentDraft = async (fields) => { const { draftId, isReply, isEdit, isPostSending } = this.state; @@ -494,15 +515,15 @@ class EditorContainer extends Component { tags: fields.tags && fields.tags.length > 0 ? fields.tags.toString() : '', author: username, meta: fields.meta && fields.meta, - } + }; //save reply data if (isReply && draftField.body !== null) { - dispatch(updateDraftCache(draftId, draftField)) + dispatch(updateDraftCache(draftId, draftField)); //save existing draft data locally } else if (draftId) { - dispatch(updateDraftCache(draftId, draftField)) + dispatch(updateDraftCache(draftId, draftField)); } //update editor data locally @@ -511,12 +532,7 @@ class EditorContainer extends Component { } }; - - - - - _submitPost = async ({ fields, scheduleDate }: { fields: any, scheduleDate?: string }) => { - + _submitPost = async ({ fields, scheduleDate }: { fields: any; scheduleDate?: string }) => { const { currentAccount, dispatch, @@ -525,11 +541,10 @@ class EditorContainer extends Component { pinCode, // isDefaultFooter, } = this.props; - const { rewardType, isPostSending, thumbIndex, draftId, shouldReblog } = this.state; + const { rewardType, isPostSending, thumbUrl, draftId, shouldReblog } = this.state; const beneficiaries = this._extractBeneficiaries(); - if (isPostSending) { return; } @@ -539,7 +554,7 @@ class EditorContainer extends Component { isPostSending: true, }); - const meta = extractMetadata(fields.body, thumbIndex); + const meta = extractMetadata(fields.body, thumbUrl); const _tags = fields.tags.filter((tag) => tag && tag !== ' '); const jsonMeta = makeJsonMetadata(meta, _tags); @@ -553,7 +568,7 @@ class EditorContainer extends Component { dublicatePost = null; } - if (dublicatePost && (dublicatePost.permlink === permlink)) { + if (dublicatePost && dublicatePost.permlink === permlink) { permlink = generatePermlink(fields.title, true); } @@ -592,29 +607,25 @@ class EditorContainer extends Component { voteWeight, ) .then((response) => { - console.log(response); //reblog if flag is active if (shouldReblog) { - reblog( - currentAccount, - pinCode, - author, - permlink - ).then((resp) => { - console.log("Successfully reblogged post", resp) - }).catch((err) => { - console.warn("Failed to reblog post", err) - }) + reblog(currentAccount, pinCode, author, permlink) + .then((resp) => { + console.log('Successfully reblogged post', resp); + }) + .catch((err) => { + console.warn('Failed to reblog post', err); + }); } //post publish updates - dispatch(deleteDraftCacheEntry(DEFAULT_USER_DRAFT_ID + currentAccount.name)) + dispatch(deleteDraftCacheEntry(DEFAULT_USER_DRAFT_ID + currentAccount.name)); - dispatch(removeBeneficiaries(TEMP_BENEFICIARIES_ID)) + dispatch(removeBeneficiaries(TEMP_BENEFICIARIES_ID)); if (draftId) { - dispatch(removeBeneficiaries(draftId)) + dispatch(removeBeneficiaries(draftId)); } dispatch( @@ -632,9 +643,10 @@ class EditorContainer extends Component { ROUTES.SCREENS.PROFILE, { username: get(currentAccount, 'name'), - }, { - key: get(currentAccount, 'name') - } + }, + { + key: get(currentAccount, 'name'), + }, ); }, 3000); }) @@ -665,7 +677,6 @@ class EditorContainer extends Component { const parentPermlink = post.permlink; const parentTags = post.json_metadata.tags; - await postComment( currentAccount, pinCode, @@ -691,11 +702,10 @@ class EditorContainer extends Component { markdownBody: fields.body, }, { - parentTags: parentTags || ['ecency'] - } - ) - ) - + parentTags: parentTags || ['ecency'], + }, + ), + ); }) .catch((error) => { this._handleSubmitFailure(error); @@ -705,7 +715,7 @@ class EditorContainer extends Component { _submitEdit = async (fields) => { const { currentAccount, pinCode, dispatch } = this.props; - const { post, isEdit, isPostSending, thumbIndex, isReply } = this.state; + const { post, isEdit, isPostSending, thumbUrl, isReply } = this.state; if (isPostSending) { return; @@ -731,7 +741,7 @@ class EditorContainer extends Component { newBody = patch; } - const meta = extractMetadata(fields.body, thumbIndex); + const meta = extractMetadata(fields.body, thumbUrl); let jsonMeta = {}; @@ -773,13 +783,13 @@ class EditorContainer extends Component { author_reputation: post.author_reputation, total_payout: post.total_payout, created: post.created, - json_metadata: jsonMeta + json_metadata: jsonMeta, }, { - isUpdate: true - } - ) - ) + isUpdate: true, + }, + ), + ); } }) .catch((error) => { @@ -799,10 +809,7 @@ class EditorContainer extends Component { ) { //when RC is not enough, offer boosting account dispatch(setRcOffer(true)); - } else if ( - error && - error.jse_shortmsg && - error.jse_shortmsg.includes('wait to transact')) { + } else if (error && error.jse_shortmsg && error.jse_shortmsg.includes('wait to transact')) { //when RC is not enough, offer boosting account dispatch(setRcOffer(true)); } else { @@ -840,20 +847,10 @@ class EditorContainer extends Component { }, 3000); }; - _navigationBackFetchDrafts = () => { - const { route } = this.props; - const { isDraft } = this.state; - - if (isDraft && route.params?.fetchPost) { - route.params.fetchPost - } - }; - _handleSubmit = (form: any) => { const { isReply, isEdit } = this.state; const { intl } = this.props; - if (isReply && !isEdit) { this._submitReply(form.fields); } else if (isEdit) { @@ -909,8 +906,6 @@ class EditorContainer extends Component { } }; - - _handleFormChanged = () => { const { isDraftSaved } = this.state; @@ -921,7 +916,6 @@ class EditorContainer extends Component { } }; - _handleSchedulePress = async (datePickerValue, fields) => { const { currentAccount, pinCode, intl } = this.props; @@ -998,13 +992,12 @@ class EditorContainer extends Component { ), ); - dispatch(deleteDraftCacheEntry(DEFAULT_USER_DRAFT_ID + currentAccount.name)) + dispatch(deleteDraftCacheEntry(DEFAULT_USER_DRAFT_ID + currentAccount.name)); setTimeout(() => { - navigation.replace(ROUTES.SCREENS.DRAFTS, - { - showSchedules: true - }) + navigation.replace(ROUTES.SCREENS.DRAFTS, { + showSchedules: true, + }); }, 3000); }) .catch((error) => { @@ -1018,10 +1011,10 @@ class EditorContainer extends Component { _initialEditor = () => { const { currentAccount: { name }, - dispatch + dispatch, } = this.props; - dispatch(deleteDraftCacheEntry(DEFAULT_USER_DRAFT_ID + name)) + dispatch(deleteDraftCacheEntry(DEFAULT_USER_DRAFT_ID + name)); this.setState({ uploadedImage: null, @@ -1032,26 +1025,23 @@ class EditorContainer extends Component { this.setState({ rewardType: value }); }; - _handleShouldReblogChange = (value: boolean) => { this.setState({ - shouldReblog: value - }) - } + shouldReblog: value, + }); + }; - - _handleSetThumbIndex = (index: number) => { + _handleSetThumbUrl = (url: string) => { this.setState({ - thumbIndex: index - }) - } + thumbUrl: url, + }); + }; - _setIsUploading = (status:boolean) => { + _setIsUploading = (status: boolean) => { this.setState({ - isUploading:status - }) - } - + isUploading: status, + }); + }; render() { const { isLoggedIn, isDarkTheme, currentAccount, route } = this.props; @@ -1072,13 +1062,13 @@ class EditorContainer extends Component { community, sharedSnippetText, onLoadDraftPress, - thumbIndex, + thumbUrl, uploadProgress, rewardType, } = this.state; const tags = route.params?.tags; - const paramFiles = route.params?.files; + const paramFiles = route.params?.files; return ( { handleShouldReblogChange={this._handleShouldReblogChange} handleSchedulePress={this._handleSchedulePress} handleFormChanged={this._handleFormChanged} - handleOnBackPress={() => { }} + handleOnBackPress={() => {}} handleOnSubmit={this._handleSubmit} initialEditor={this._initialEditor} isDarkTheme={isDarkTheme} @@ -1102,7 +1092,7 @@ class EditorContainer extends Component { quickReplyText={quickReplyText} isUploading={isUploading} post={post} - updateDraftFields = {this._updateDraftFields} + updateDraftFields={this._updateDraftFields} saveCurrentDraft={this._saveCurrentDraft} saveDraftToDB={this._saveDraftToDB} uploadedImage={uploadedImage} @@ -1112,8 +1102,8 @@ class EditorContainer extends Component { draftId={draftId} sharedSnippetText={sharedSnippetText} onLoadDraftPress={onLoadDraftPress} - thumbIndex={thumbIndex} - setThumbIndex={this._handleSetThumbIndex} + thumbUrl={thumbUrl} + setThumbUrl={this._handleSetThumbUrl} uploadProgress={uploadProgress} rewardType={rewardType} getBeneficiaries={this._extractBeneficiaries} @@ -1132,4 +1122,10 @@ const mapStateToProps = (state) => ({ drafts: state.cache.drafts, }); -export default connect(mapStateToProps)(injectIntl(EditorContainer)); +export default connect(mapStateToProps)( + injectIntl( + //NOTE: remove extra integration step once compoent converted to functional component + //TOOD: inject add and update draft mutation hooks as well + (props) => , + ), +); diff --git a/src/screens/editor/screen/editorScreen.tsx b/src/screens/editor/screen/editorScreen.tsx index eb542df64..c1820f634 100644 --- a/src/screens/editor/screen/editorScreen.tsx +++ b/src/screens/editor/screen/editorScreen.tsx @@ -143,23 +143,23 @@ class EditorScreen extends Component { }; _handleOnSaveButtonPress = () => { - const {draftId, intl} = this.props; - if(draftId){ - Alert.alert( - intl.formatMessage({id:'editor.draft_save_title'}), - "", - [{ - text:intl.formatMessage({id:'editor.draft_update'}), - onPress:()=>this._saveDraftToDB(), - },{ - text:intl.formatMessage({id:'editor.draft_save_new'}), - onPress:()=>this._saveDraftToDB(true) - },{ - text:intl.formatMessage({id:'alert.cancel'}), - onPress:()=>{}, - style:'cancel' - }] - ) + const { draftId, intl } = this.props; + if (draftId) { + Alert.alert(intl.formatMessage({ id: 'editor.draft_save_title' }), '', [ + { + text: intl.formatMessage({ id: 'editor.draft_update' }), + onPress: () => this._saveDraftToDB(), + }, + { + text: intl.formatMessage({ id: 'editor.draft_save_new' }), + onPress: () => this._saveDraftToDB(true), + }, + { + text: intl.formatMessage({ id: 'alert.cancel' }), + onPress: () => {}, + style: 'cancel', + }, + ]); return; } this._saveDraftToDB(); @@ -174,7 +174,7 @@ class EditorScreen extends Component { this.changeTimer = setTimeout(() => { // saveCurrentDraft(fields); - updateDraftFields(fields) + updateDraftFields(fields); }, 300); }; @@ -182,7 +182,7 @@ class EditorScreen extends Component { const { handleOnSubmit, handleSchedulePress } = this.props; const { fields, scheduledFor } = this.state; - if(scheduledFor && handleSchedulePress){ + if (scheduledFor && handleSchedulePress) { handleSchedulePress(scheduledFor, fields); return; } @@ -192,29 +192,28 @@ class EditorScreen extends Component { } }; - _handleOnThumbSelection = (index) => { - const { setThumbIndex } = this.props; - if (setThumbIndex) { - setThumbIndex(index); + _handleOnThumbSelection = (url: string) => { + const { setThumbUrl } = this.props; + if (setThumbUrl) { + setThumbUrl(url); } }; - - _handleScheduleChange = (datetime:string|null) => { + _handleScheduleChange = (datetime: string | null) => { this.setState({ - scheduledFor:datetime, - }) - } + scheduledFor: datetime, + }); + }; _handleRewardChange = (value) => { const { handleRewardChange } = this.props; handleRewardChange(value); - } + }; _handleSettingsPress = () => { - if(this.postOptionsModalRef){ + if (this.postOptionsModalRef) { this.postOptionsModalRef.show(); } - } + }; _handleIsFormValid = (bodyText) => { const { fields } = this.state; @@ -236,7 +235,7 @@ class EditorScreen extends Component { }; _handleFormUpdate = (componentID, content) => { - const { handleFormChanged, thumbIndex, rewardType, getBeneficiaries } = this.props; + const { handleFormChanged, thumbUrl, rewardType, getBeneficiaries } = this.props; const { fields: _fields } = this.state; const fields = { ..._fields }; @@ -248,14 +247,14 @@ class EditorScreen extends Component { fields.tags = content; } - const meta = Object.assign({}, extractMetadata(fields.body, thumbIndex), { + const meta = Object.assign({}, extractMetadata(fields.body, thumbUrl), { tags: fields.tags, beneficiaries: getBeneficiaries(), rewardType, }); const jsonMeta = makeJsonMetadata(meta, fields.tags); fields.meta = jsonMeta; - + if ( get(fields, 'body', '').trim() !== get(_fields, 'body', '').trim() || get(fields, 'title', '').trim() !== get(_fields, 'title', '').trim() || @@ -264,7 +263,7 @@ class EditorScreen extends Component { ) { console.log('jsonMeta : ', jsonMeta); handleFormChanged(); - + this._saveCurrentDraft(fields); } @@ -338,7 +337,7 @@ class EditorScreen extends Component { }); }; - _saveDraftToDB(saveAsNew?:boolean) { + _saveDraftToDB(saveAsNew?: boolean) { const { saveDraftToDB } = this.props; const { fields } = this.state; @@ -381,17 +380,22 @@ class EditorScreen extends Component { autoFocusText, sharedSnippetText, onLoadDraftPress, - thumbIndex, + thumbUrl, uploadProgress, rewardType, setIsUploading, } = this.props; const rightButtonText = intl.formatMessage({ - id: isEdit ? 'basic_header.update' : isReply ? 'basic_header.reply' : scheduledFor ? 'basic_header.schedule' : 'basic_header.publish', + id: isEdit + ? 'basic_header.update' + : isReply + ? 'basic_header.reply' + : scheduledFor + ? 'basic_header.schedule' + : 'basic_header.publish', }); - const _renderCommunityModal = () => { return ( (this.postOptionsModalRef = componentRef)} body={fields.body} draftId={draftId} - thumbIndex={thumbIndex} + thumbUrl={thumbUrl} isEdit={isEdit} isCommunityPost={selectedCommunity !== null} rewardType={rewardType} diff --git a/src/utils/editor.ts b/src/utils/editor.ts index 8c7043543..ea744bb52 100644 --- a/src/utils/editor.ts +++ b/src/utils/editor.ts @@ -1,7 +1,6 @@ import getSlug from 'speakingurl'; import { diff_match_patch as diffMatchPatch } from 'diff-match-patch'; import VersionNumber from 'react-native-version-number'; -import { PanGestureHandler } from 'react-native-gesture-handler'; import MimeTypes from 'mime-types'; export const getWordsCount = (text) => @@ -46,8 +45,7 @@ export const generatePermlink = (title, random = false) => { return perm; }; -;export const extractWordAtIndex = (text:string, index:number) => { - +export const extractWordAtIndex = (text: string, index: number) => { const RANGE = 50; const _start = index - RANGE; @@ -56,32 +54,30 @@ export const generatePermlink = (title, random = false) => { const _length = text.length; const textChunk = text.substring(_start > 0 ? _start : 0, _end < _length ? _end : _length); - const indexChunk = index < 50 ? index : ( - _length - index < 50 ? textChunk.length - (_length - index) : - RANGE - ); + const indexChunk = + index < 50 ? index : _length - index < 50 ? textChunk.length - (_length - index) : RANGE; console.log('char at index: ', textChunk[indexChunk]); - const END_REGEX = /[\s,]/ + const END_REGEX = /[\s,]/; let word = ''; - for(let i = indexChunk; i >= 0 && (!END_REGEX.test(textChunk[i]) || i === indexChunk); i--){ - if(textChunk[i]){ + for (let i = indexChunk; i >= 0 && (!END_REGEX.test(textChunk[i]) || i === indexChunk); i--) { + if (textChunk[i]) { word += textChunk[i]; } } word = word.split('').reverse().join(''); - - if(!END_REGEX.test(textChunk[indexChunk])){ - for(let i = indexChunk + 1; i < textChunk.length && !END_REGEX.test(textChunk[i]); i++){ - if(textChunk[i]){ + + if (!END_REGEX.test(textChunk[indexChunk])) { + for (let i = indexChunk + 1; i < textChunk.length && !END_REGEX.test(textChunk[i]); i++) { + if (textChunk[i]) { word += textChunk[i]; } } } - + return word; -} +}; export const generateReplyPermlink = (toAuthor) => { if (!toAuthor) { @@ -169,52 +165,55 @@ export const makeJsonMetadataForUpdate = (oldJson, meta, tags) => { return Object.assign({}, oldJson, mergedMeta, { tags }); }; - -const extractUrls = (body:string) => { +const extractUrls = (body: string) => { const urlReg = /(\b(https?|ftp):\/\/[A-Z0-9+&@#/%?=~_|!:,.;-]*[-A-Z0-9+&@#/%=~_|])/gim; const mUrls = body && body.match(urlReg); return mUrls || []; -} +}; - -export const extractImageUrls = ({body, urls}:{body?:string, urls?:string[]}) => { +export const extractImageUrls = ({ body, urls }: { body?: string; urls?: string[] }) => { const imgReg = /(https?:\/\/.*\.(?:png|jpg|jpeg|gif|heic|webp))/gim; let imgUrls = []; const mUrls = urls || extractUrls(body); - mUrls.forEach((url)=>{ + mUrls.forEach((url) => { const isImage = url.match(imgReg); if (isImage) { imgUrls.push(url); } - }) + }); return imgUrls; -} +}; -export const extractFilenameFromPath = ({path, mimeType}:{path:string, mimeType?:string}) => { - try{ - if(!path){ - throw new Error("path not provided"); +export const extractFilenameFromPath = ({ + path, + mimeType, +}: { + path: string; + mimeType?: string; +}) => { + try { + if (!path) { + throw new Error('path not provided'); } const filenameIndex = path.lastIndexOf('/') + 1; const extensionIndex = path.lastIndexOf('.'); - if(filenameIndex < 0 || extensionIndex <= filenameIndex){ - throw new Error("file name not present with extension"); + if (filenameIndex < 0 || extensionIndex <= filenameIndex) { + throw new Error('file name not present with extension'); } return path.substring(path.lastIndexOf('/') + 1); - - }catch(err){ + } catch (err) { let _ext = 'jpg'; - if(mimeType){ - _ext = MimeTypes.extension(mimeType) + if (mimeType) { + _ext = MimeTypes.extension(mimeType); } return `${generateRndStr()}.${_ext}`; } -} +}; -export const extractMetadata = (body:string, thumbIndex?:number) => { +export const extractMetadata = (body: string, thumbUrl?: string) => { const userReg = /(^|\s)(@[a-z][-.a-z\d]+[a-z\d])/gim; const out = {}; @@ -227,11 +226,11 @@ export const extractMetadata = (body:string, thumbIndex?:number) => { const matchedUsers = []; if (mUrls) { - mUrls.forEach((url)=>{ - if(matchedImages.indexOf(url) < 0){ + mUrls.forEach((url) => { + if (matchedImages.indexOf(url) < 0) { matchedLinks.push(url); } - }) + }); } if (matchedLinks.length) { @@ -239,10 +238,10 @@ export const extractMetadata = (body:string, thumbIndex?:number) => { } if (matchedImages.length) { - if(thumbIndex){ - matchedImages.splice(0, 0, matchedImages.splice(thumbIndex, 1)[0]); + if (thumbUrl) { + matchedImages.sort((item) => (item === thumbUrl ? -1 : 1)); } - + out.image = matchedImages; } @@ -271,4 +270,4 @@ export const createPatch = (text1, text2) => { return patch; }; -export const delay = ms => new Promise(res => setTimeout(res, ms)); +export const delay = (ms) => new Promise((res) => setTimeout(res, ms)); diff --git a/src/utils/wallet.ts b/src/utils/wallet.ts index 8a708fa4f..f82d48037 100644 --- a/src/utils/wallet.ts +++ b/src/utils/wallet.ts @@ -1,21 +1,39 @@ import get from 'lodash/get'; +import { operationOrders } from '@hiveio/dhive/lib/utils'; +import { utils } from '@hiveio/dhive'; import parseDate from './parseDate'; import parseToken from './parseToken'; import { vestsToHp } from './conversions'; -import { getAccount, getAccountHistory, getConversionRequests, getFeedHistory, getOpenOrders, getSavingsWithdrawFrom } from '../providers/hive/dhive'; +import { + fetchGlobalProps, + getAccount, + getAccountHistory, + getConversionRequests, + getFeedHistory, + getOpenOrders, + getSavingsWithdrawFrom, +} from '../providers/hive/dhive'; import { getCurrencyTokenRate, getLatestQuotes } from '../providers/ecency/ecency'; -import { CoinActivitiesCollection, CoinActivity, CoinBase, CoinData, DataPair, QuoteItem } from '../redux/reducers/walletReducer'; +import { + CoinActivitiesCollection, + CoinActivity, + CoinBase, + CoinData, + DataPair, + QuoteItem, +} from '../redux/reducers/walletReducer'; import { GlobalProps } from '../redux/reducers/accountReducer'; import { getEstimatedAmount } from './vote'; import { getPointsSummary, getPointsHistory } from '../providers/ecency/ePoint'; // Constant import POINTS from '../constants/options/points'; import { COIN_IDS } from '../constants/defaultCoins'; -import { operationOrders } from '@hiveio/dhive/lib/utils'; -import { ConversionRequest, OpenOrderItem, OrdersData, SavingsWithdrawRequest } from '../providers/hive/hive.types'; +import { + ConversionRequest, + OpenOrderItem, + SavingsWithdrawRequest, +} from '../providers/hive/hive.types'; import parseAsset from './parseAsset'; -import { utils } from '@hiveio/dhive'; - export const transferTypes = [ 'curation_reward', @@ -39,24 +57,16 @@ export const transferTypes = [ 'fill_vesting_withdraw', ]; -const ECENCY_ACTIONS = [ - 'dropdown_transfer', 'dropdown_promote', 'dropdown_boost' -]; +const ECENCY_ACTIONS = ['dropdown_transfer', 'dropdown_promote', 'dropdown_boost']; const HIVE_ACTIONS = [ 'transfer_token', 'transfer_to_savings', 'transfer_to_vesting', - 'withdraw_hive' -]; -const HBD_ACTIONS = [ - 'transfer_token', - 'transfer_to_savings', - 'convert', - 'withdraw_hbd' + 'withdraw_hive', ]; +const HBD_ACTIONS = ['transfer_token', 'transfer_to_savings', 'convert', 'withdraw_hbd']; const HIVE_POWER_ACTIONS = ['delegate', 'power_down']; - export const groomingTransactionData = (transaction, hivePerMVests) => { if (!transaction || !hivePerMVests) { return []; @@ -64,7 +74,7 @@ export const groomingTransactionData = (transaction, hivePerMVests) => { const result = { iconType: 'MaterialIcons', - trxIndex:transaction[0] + trxIndex: transaction[0], }; [result.textKey] = transaction[1].op; @@ -102,8 +112,9 @@ export const groomingTransactionData = (transaction, hivePerMVests) => { .toFixed(3) .replace(',', '.'); - result.value = `${hbdPayout > 0 ? `${hbdPayout} HBD` : ''} ${hivePayout > 0 ? `${hivePayout} HIVE` : '' - } ${vestingPayout > 0 ? `${vestingPayout} HP` : ''}`; + result.value = `${hbdPayout > 0 ? `${hbdPayout} HBD` : ''} ${ + hivePayout > 0 ? `${hivePayout} HIVE` : '' + } ${vestingPayout > 0 ? `${vestingPayout} HP` : ''}`; result.details = author && permlink ? `@${author}/${permlink}` : null; if (result.textKey === 'comment_benefactor_reward') { @@ -117,8 +128,9 @@ export const groomingTransactionData = (transaction, hivePerMVests) => { rewardHive = parseToken(rewardHive).toFixed(3).replace(',', '.'); rewardVests = vestsToHp(parseToken(rewardVests), hivePerMVests).toFixed(3).replace(',', '.'); - result.value = `${rewardHdb > 0 ? `${rewardHdb} HBD` : ''} ${rewardHive > 0 ? `${rewardHive} HIVE` : '' - } ${rewardVests > 0 ? `${rewardVests} HP` : ''}`; + result.value = `${rewardHdb > 0 ? `${rewardHdb} HBD` : ''} ${ + rewardHive > 0 ? `${rewardHive} HIVE` : '' + } ${rewardVests > 0 ? `${rewardVests} HP` : ''}`; break; case 'transfer': case 'transfer_to_savings': @@ -263,7 +275,7 @@ export const groomingWalletData = async (user, globalProps, userCurrency) => { walletData.nextVestingWithdrawal = Math.round(timeDiff / (1000 * 3600)); //TOOD: transfer history can be separated from here - const op = utils.operationOrders + const op = utils.operationOrders; const ops = [ op.transfer, //HIVE op.author_reward, //HBD, HP @@ -279,7 +291,7 @@ export const groomingWalletData = async (user, globalProps, userCurrency) => { op.sps_fund, //HBD op.comment_benefactor_reward, //HP op.return_vesting_delegation, //HP - ] + ]; const history = await getAccountHistory(get(user, 'name'), ops); @@ -291,10 +303,10 @@ export const groomingWalletData = async (user, globalProps, userCurrency) => { return walletData; }; - - -const fetchPendingRequests = async (username: string, coinSymbol: string): Promise => { - +const fetchPendingRequests = async ( + username: string, + coinSymbol: string, +): Promise => { const _rawConversions = await getConversionRequests(username); const _rawOpenOrdres = await getOpenOrders(username); const _rawWithdrawRequests = await getSavingsWithdrawFrom(username); @@ -302,66 +314,63 @@ const fetchPendingRequests = async (username: string, coinSymbol: string): Promi console.log('fetched pending requests', _rawConversions, _rawOpenOrdres, _rawWithdrawRequests); const openOrderRequests = _rawOpenOrdres - .filter(request => request.sell_price.base.includes(coinSymbol)) + .filter((request) => request.sell_price.base.includes(coinSymbol)) .map((request) => { const { base, quote } = request?.sell_price || {}; - return ({ - iconType: "MaterialIcons", + return { + iconType: 'MaterialIcons', textKey: 'open_order', expires: request.expiration, created: request.created, icon: 'reorder', value: base || '-- --', details: base && quote ? `@ ${base} = ${quote}` : '', - } as CoinActivity) - }) + } as CoinActivity; + }); const withdrawRequests = _rawWithdrawRequests - .filter(request => request.amount.includes(coinSymbol)) + .filter((request) => request.amount.includes(coinSymbol)) .map((request) => { - return ({ - iconType: "MaterialIcons", - textKey: "withdraw_savings", + return { + iconType: 'MaterialIcons', + textKey: 'withdraw_savings', created: request.complete, - icon: "compare-arrows", + icon: 'compare-arrows', value: request.amount, details: request.from && request.to ? `@${request.from} to @${request.to}` : null, - memo: request.memo || null - } as CoinActivity) - }) + memo: request.memo || null, + } as CoinActivity; + }); const conversionRequests = _rawConversions - .filter(request => request.amount.includes(coinSymbol)) + .filter((request) => request.amount.includes(coinSymbol)) .map((request) => { - return ({ - iconType: "MaterialIcons", - textKey: "convert_request", + return { + iconType: 'MaterialIcons', + textKey: 'convert_request', created: request.conversion_date, - icon: "hourglass-full", - value: request.amount - } as CoinActivity) - }) + icon: 'hourglass-full', + value: request.amount, + } as CoinActivity; + }); - const pendingRequests = [ - ...openOrderRequests, - ...withdrawRequests, - ...conversionRequests - ]; + const pendingRequests = [...openOrderRequests, ...withdrawRequests, ...conversionRequests]; - pendingRequests.sort((a, b) => ( - new Date(a.expires || a.created).getTime() > new Date(b.expires || b.created).getTime() ? 1 : -1 - )) + pendingRequests.sort((a, b) => + new Date(a.expires || a.created).getTime() > new Date(b.expires || b.created).getTime() + ? 1 + : -1, + ); return pendingRequests; -} - +}; /** - * - * @param username - * @param coinId - * @param coinSymbol - * @param globalProps + * + * @param username + * @param coinId + * @param coinSymbol + * @param globalProps * @returns {Promise} */ export const fetchCoinActivities = async ( @@ -370,131 +379,109 @@ export const fetchCoinActivities = async ( coinSymbol: string, globalProps: GlobalProps, startIndex: number, - limit:number - + limit: number, ): Promise => { - const op = operationOrders; let history = []; switch (coinId) { case COIN_IDS.ECENCY: { - //TODO: remove condition when we have a way to fetch paginated points data - if(startIndex !== -1){ + if (startIndex !== -1) { return { - completed:[], - pending:[] - } + completed: [], + pending: [], + }; } const pointActivities = await getPointsHistory(username); - console.log("Points Activities", pointActivities); - const completed = pointActivities && pointActivities.length ? - pointActivities.map((item) => - groomingPointsTransactionData({ - ...item, - icon: get(POINTS[get(item, 'type')], 'icon'), - iconType: get(POINTS[get(item, 'type')], 'iconType'), - textKey: get(POINTS[get(item, 'type')], 'textKey'), - }) - ) : []; + console.log('Points Activities', pointActivities); + const completed = + pointActivities && pointActivities.length + ? pointActivities.map((item) => + groomingPointsTransactionData({ + ...item, + icon: get(POINTS[get(item, 'type')], 'icon'), + iconType: get(POINTS[get(item, 'type')], 'iconType'), + textKey: get(POINTS[get(item, 'type')], 'textKey'), + }), + ) + : []; return { completed, - pending: [] as CoinActivity[] - } + pending: [] as CoinActivity[], + }; } case COIN_IDS.HIVE: - history = await getAccountHistory(username, [ - op.transfer, //HIVE - op.transfer_to_vesting, //HIVE, HP - op.withdraw_vesting, //HIVE, HP - op.transfer_to_savings, //HIVE, HBD - op.transfer_from_savings, //HIVE, HBD - op.fill_order, //HIVE, HBD - ], startIndex, limit); + history = await getAccountHistory( + username, + [ + op.transfer, //HIVE + op.transfer_to_vesting, //HIVE, HP + op.withdraw_vesting, //HIVE, HP + op.transfer_to_savings, //HIVE, HBD + op.transfer_from_savings, //HIVE, HBD + op.fill_order, //HIVE, HBD + ], + startIndex, + limit, + ); break; case COIN_IDS.HBD: - history = await getAccountHistory(username, [ - op.transfer, //HIVE //HBD - op.author_reward, //HBD, HP - op.transfer_to_savings, //HIVE, HBD - op.transfer_from_savings, //HIVE, HBD - op.fill_convert_request, //HBD - op.fill_order, //HIVE, HBD - op.sps_fund, //HBD - ], startIndex, limit); + history = await getAccountHistory( + username, + [ + op.transfer, //HIVE //HBD + op.author_reward, //HBD, HP + op.transfer_to_savings, //HIVE, HBD + op.transfer_from_savings, //HIVE, HBD + op.fill_convert_request, //HBD + op.fill_order, //HIVE, HBD + op.sps_fund, //HBD + ], + startIndex, + limit, + ); break; case COIN_IDS.HP: - history = await getAccountHistory(username, [ - op.author_reward, //HBD, HP - op.curation_reward, //HP - op.transfer_to_vesting, //HIVE, HP - op.withdraw_vesting, //HIVE, HP - op.interest, //HP - op.claim_reward_balance, //HP - op.comment_benefactor_reward, //HP - op.return_vesting_delegation, //HP - ], startIndex, limit); + history = await getAccountHistory( + username, + [ + op.author_reward, //HBD, HP + op.curation_reward, //HP + op.transfer_to_vesting, //HIVE, HP + op.withdraw_vesting, //HIVE, HP + op.interest, //HP + op.claim_reward_balance, //HP + op.comment_benefactor_reward, //HP + op.return_vesting_delegation, //HP + ], + startIndex, + limit, + ); break; } - const transfers = history.filter((tx) => transferTypes.includes(get(tx[1], 'op[0]', false))); transfers.sort(compare); - const activities = transfers.map(item => groomingTransactionData(item, globalProps.hivePerMVests)); + const activities = transfers.map((item) => + groomingTransactionData(item, globalProps.hivePerMVests), + ); const filterdActivities: CoinActivity[] = activities ? activities.filter((item) => { - return ( - item && - item.value && - item.value.includes(coinSymbol) - ); - }) + return item && item.value && item.value.includes(coinSymbol); + }) : []; - console.log('FILTERED comap', activities.length, filterdActivities.length) + console.log('FILTERED comap', activities.length, filterdActivities.length); const pendingRequests = await fetchPendingRequests(username, coinSymbol); return { completed: filterdActivities, pending: pendingRequests, - } - -} - - - - - -const calculateConvertingAmount = (requests: ConversionRequest[]): number => { - if (!requests || !requests.length) { - return 0; - } - //TODO: add method body - // ecency-vision -> src/common/components/wallet-hive/index.tsx#fetchConvertingAmount - throw new Error("calculateConvertingAmount method body not implemented yet"); -} - -const calculateSavingsWithdrawalAmount = (requests: SavingsWithdrawRequest[], coinSymbol: string): number => { - return requests.reduce((prevVal, curRequest) => { - const _amount = curRequest.amount; - return _amount.includes(coinSymbol) - ? prevVal + parseAsset(_amount).amount - : prevVal - }, 0); -} - -const calculateOpenOrdersAmount = (requests: OpenOrderItem[], coinSymbol: string): number => { - return requests.reduce((prevVal, curRequest) => { - const _basePrice = curRequest.sell_price.base; - return _basePrice.includes(coinSymbol) - ? prevVal + parseAsset(_basePrice).amount - : prevVal - }, 0); -} - + }; +}; export const fetchCoinsData = async ({ coins, @@ -505,36 +492,32 @@ export const fetchCoinsData = async ({ refresh, quotes, }: { - coins: CoinBase[], - currentAccount: any, - vsCurrency: string, - currencyRate: number, - globalProps: GlobalProps, - quotes: { [key: string]: QuoteItem } - refresh: boolean, -}) - : Promise<{ [key: string]: CoinData }> => { - + coins: CoinBase[]; + currentAccount: any; + vsCurrency: string; + currencyRate: number; + globalProps: GlobalProps; + quotes: { [key: string]: QuoteItem }; + refresh: boolean; +}): Promise<{ [key: string]: CoinData }> => { const username = currentAccount.username; - const { base, quote, hivePerMVests } = globalProps - const coinData = {} as { [key: string]: CoinData }; const walletData = {} as any; - if (!username) { return walletData; } + //fetch latest global props if refresh or data not available + const { base, quote, hivePerMVests } = + refresh || !globalProps || !globalProps.hivePerMVests ? await fetchGlobalProps() : globalProps; //TODO: Use already available accoutn for frist wallet start const userdata = refresh ? await getAccount(username) : currentAccount; - const _pointsSummary = refresh ? await getPointsSummary(username) : currentAccount.pointsSummary + const _pointsSummary = refresh ? await getPointsSummary(username) : currentAccount.pointsSummary; //TODO: cache data in redux or fetch once on wallet startup const _prices = !refresh && quotes ? quotes : await getLatestQuotes(currencyRate); //TODO: figure out a way to handle other currencies - coins.forEach((coinBase) => { - switch (coinBase.id) { case COIN_IDS.ECENCY: { const balance = _pointsSummary.points ? parseFloat(_pointsSummary.points) : 0; @@ -549,7 +532,7 @@ export const fetchCoinsData = async ({ currentPrice: ppEstm, unclaimedBalance: unclaimedBalance, actions: ECENCY_ACTIONS, - } + }; break; } case COIN_IDS.HIVE: { @@ -557,7 +540,6 @@ export const fetchCoinsData = async ({ const savings = parseToken(userdata.savings_balance); const ppHive = _prices[coinBase.id].price; - coinData[coinBase.id] = { balance: Math.round(balance * 1000) / 1000, estimateValue: (balance + savings) * ppHive, @@ -566,7 +548,7 @@ export const fetchCoinsData = async ({ currentPrice: ppHive, unclaimedBalance: '', actions: HIVE_ACTIONS, - } + }; break; } @@ -583,33 +565,27 @@ export const fetchCoinsData = async ({ currentPrice: ppHbd, unclaimedBalance: '', actions: HBD_ACTIONS, - } + }; break; } case COIN_IDS.HP: { - const _getBalanceStr = (val: number, cur: string) => (val ? Math.round(val * 1000) / 1000 + cur : ''); - const balance = Math.round( - vestsToHp(parseToken(userdata.vesting_shares), hivePerMVests) * 1000, - ) / 1000; + const _getBalanceStr = (val: number, cur: string) => + val ? Math.round(val * 1000) / 1000 + cur : ''; + const balance = + Math.round(vestsToHp(parseToken(userdata.vesting_shares), hivePerMVests) * 1000) / 1000; - const receivedHP = vestsToHp( - parseToken(userdata.received_vesting_shares), - hivePerMVests, - ) + const receivedHP = vestsToHp(parseToken(userdata.received_vesting_shares), hivePerMVests); - const delegatedHP = vestsToHp( - parseToken(userdata.delegated_vesting_shares), - hivePerMVests, - ) + const delegatedHP = vestsToHp(parseToken(userdata.delegated_vesting_shares), hivePerMVests); //agggregate claim button text const unclaimedBalance = [ _getBalanceStr(parseToken(userdata.reward_hive_balance), ' HIVE'), _getBalanceStr(parseToken(userdata.reward_hbd_balance), ' HBD'), - _getBalanceStr(parseToken(userdata.reward_vesting_hive), ' HP') + _getBalanceStr(parseToken(userdata.reward_vesting_hive), ' HP'), ].reduce( - (prevVal, bal) => prevVal + (!bal ? '' : (`${prevVal !== '' ? ' ' : ''}${bal}`)), - '' + (prevVal, bal) => prevVal + (!bal ? '' : `${prevVal !== '' ? ' ' : ''}${bal}`), + '', ); //calculate power down @@ -619,49 +595,57 @@ export const fetchCoinsData = async ({ const nextVestingSharesWithdrawal = isPoweringDown ? Math.min( - parseAsset(userdata.vesting_withdraw_rate).amount, - (Number(userdata.to_withdraw) - Number(userdata.withdrawn)) / 1e6 - ) : 0; - const nextVestingSharesWithdrawalHive = isPoweringDown ? vestsToHp(nextVestingSharesWithdrawal, hivePerMVests) : 0; + parseAsset(userdata.vesting_withdraw_rate).amount, + (Number(userdata.to_withdraw) - Number(userdata.withdrawn)) / 1e6, + ) + : 0; + const nextVestingSharesWithdrawalHive = isPoweringDown + ? vestsToHp(nextVestingSharesWithdrawal, hivePerMVests) + : 0; const estimateVoteValueStr = '$ ' + getEstimatedAmount(userdata, globalProps); //aaggregate extra data pairs - const extraDataPairs:DataPair[] = []; + const extraDataPairs: DataPair[] = []; if (delegatedHP) { extraDataPairs.push({ dataKey: 'delegated_hive_power', value: `- ${delegatedHP.toFixed(3)} HP`, - isClickable: true - }) + isClickable: true, + }); } if (receivedHP) { extraDataPairs.push({ dataKey: 'received_hive_power', value: `+ ${receivedHP.toFixed(3)} HP`, - isClickable: true - }) + isClickable: true, + }); } if (nextVestingSharesWithdrawalHive) { extraDataPairs.push({ dataKey: 'powering_down_hive_power', - value: `- ${nextVestingSharesWithdrawalHive.toFixed(3)} HP` - }) + value: `- ${nextVestingSharesWithdrawalHive.toFixed(3)} HP`, + }); } extraDataPairs.concat([ { dataKey: 'total_hive_power', - value: `${(balance - delegatedHP + receivedHP - nextVestingSharesWithdrawalHive).toFixed(3)} HP` - }, { + value: `${( + balance - + delegatedHP + + receivedHP - + nextVestingSharesWithdrawalHive + ).toFixed(3)} HP`, + }, + { dataKey: 'vote_value', - value: estimateVoteValueStr - } - ]) - + value: estimateVoteValueStr, + }, + ]); const ppHive = _prices[COIN_IDS.HIVE].price; coinData[coinBase.id] = { @@ -672,22 +656,29 @@ export const fetchCoinsData = async ({ currentPrice: ppHive, actions: HIVE_POWER_ACTIONS, extraDataPairs: [ - ...extraDataPairs, { + ...extraDataPairs, + { dataKey: 'total_hive_power', - value: `${(balance - delegatedHP + receivedHP - nextVestingSharesWithdrawalHive).toFixed(3)} HP` - }, { + value: `${( + balance - + delegatedHP + + receivedHP - + nextVestingSharesWithdrawalHive + ).toFixed(3)} HP`, + }, + { dataKey: 'vote_value', - value: estimateVoteValueStr - } - ] - } + value: estimateVoteValueStr, + }, + ], + }; break; } default: break; } - }) + }); //TODO:discard unnessacry data processings towards the end of PR walletData.rewardHiveBalance = parseToken(userdata.reward_hive_balance); @@ -709,8 +700,6 @@ export const fetchCoinsData = async ({ walletData.savingBalance = parseToken(userdata.savings_balance); walletData.savingBalanceHbd = parseToken(userdata.savings_hbd_balance); - - walletData.hivePerMVests = hivePerMVests; const pricePerHive = base / quote; @@ -723,15 +712,12 @@ export const fetchCoinsData = async ({ walletData.estimatedValue = totalHive * pricePerHive + totalHbd; - - walletData.showPowerDown = userdata.next_vesting_withdrawal !== '1969-12-31T23:59:59'; const timeDiff = Math.abs(parseDate(userdata.next_vesting_withdrawal) - new Date()); walletData.nextVestingWithdrawal = Math.round(timeDiff / (1000 * 3600)); - return coinData; -} +}; function compare(a, b) { if (a[1].block < b[1].block) { diff --git a/yarn.lock b/yarn.lock index ba4a1826b..abdaedc85 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1846,6 +1846,31 @@ dependencies: type-detect "4.0.8" +"@tanstack/query-async-storage-persister@^4.3.9": + version "4.3.9" + resolved "https://registry.yarnpkg.com/@tanstack/query-async-storage-persister/-/query-async-storage-persister-4.3.9.tgz#d9954a19f41450152daf4a84c357284b36391a8a" + integrity sha512-Xn6UbUfXIpSdEMYnhgY22eYPPzNBfAGiN8WYQV/UD7lJ0iPtcX93576QON/gsqQl0oN1mwO8k38Eg1ZW+kwacA== + dependencies: + "@tanstack/react-query-persist-client" "4.3.9" + +"@tanstack/query-core@4.3.8": + version "4.3.8" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.3.8.tgz#d5f07c1d9d4f83f16f0bed7f3b245fa0e557b037" + integrity sha512-AEUWtCNBIImFZ9tMt/P8V86kIhMHpfoJqAI1auGOLR8Wzeq7Ymiue789PJG0rKYcyViUicBZeHjggMqyEQVMfQ== + +"@tanstack/react-query-persist-client@4.3.9", "@tanstack/react-query-persist-client@^4.3.9": + version "4.3.9" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-persist-client/-/react-query-persist-client-4.3.9.tgz#246acb070b8083078b6cdbf813bd6dfa2f6596e3" + integrity sha512-oFZA8bo6BQHoQqJSHXTtIDaIAxbF46cQHwhF72FwiMvBhm6eEbySUIPhGAWah7Jys2t2RJIhJ1T+q9P0RRIjwg== + +"@tanstack/react-query@^4.3.9": + version "4.3.9" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.3.9.tgz#13332a1d4dd404baec24c2853883bcb3cc61ea92" + integrity sha512-odfDW6WiSntCsCh+HFeJtUys3UnVOjfJMhykAtGtYvcklMyyDmCv9BVBt5KlSpbk/qW3kURPFCDapO+BFUlCwg== + dependencies: + "@tanstack/query-core" "4.3.8" + use-sync-external-store "^1.2.0" + "@tradle/react-native-http@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@tradle/react-native-http/-/react-native-http-2.0.1.tgz#af19e240e1e580bfa249563924d1be472686f48b" @@ -8901,11 +8926,6 @@ react-native-image-pan-zoom@^2.1.9: resolved "https://registry.yarnpkg.com/react-native-image-pan-zoom/-/react-native-image-pan-zoom-2.1.12.tgz#eb98bf56fb5610379bdbfdb63219cc1baca98fd2" integrity sha512-BF66XeP6dzuANsPmmFsJshM2Jyh/Mo1t8FsGc1L9Q9/sVP8MJULDabB1hms+eAoqgtyhMr5BuXV3E1hJ5U5H6Q== -react-native-image-size@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/react-native-image-size/-/react-native-image-size-1.1.3.tgz#7d69c2cd4e1d1632947867e47643ed8cabb9de27" - integrity sha512-jJvN6CjXVAm69LAVZNV7m7r50Qk9vuPZwLyrbs/k31/3Xs8bZyVCdvfP44FuBisITn/yFsiOo6i8NPrFBPH20w== - react-native-image-zoom-viewer@^2.2.27: version "2.2.27" resolved "https://registry.yarnpkg.com/react-native-image-zoom-viewer/-/react-native-image-zoom-viewer-2.2.27.tgz#fb3314c5dc86ac33da48cb31bf4920d97eecb6eb" @@ -10965,6 +10985,11 @@ use-subscription@^1.0.0: dependencies: object-assign "^4.1.1" +use-sync-external-store@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"