diff --git a/android/app/build.gradle b/android/app/build.gradle index 6b405b2e6c..a2da8e2491 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -102,7 +102,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode versionMajor * 10000 + versionMinor * 100 + versionPatch - versionName "3.5.1" + versionName "3.5.2" resValue "string", "build_config_package", "app.esteem.mobile.android" multiDexEnabled true // react-native-image-crop-picker diff --git a/ios/Ecency.xcodeproj/project.pbxproj b/ios/Ecency.xcodeproj/project.pbxproj index 70ed59beff..1657df4e4a 100644 --- a/ios/Ecency.xcodeproj/project.pbxproj +++ b/ios/Ecency.xcodeproj/project.pbxproj @@ -1448,7 +1448,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 26; + CURRENT_PROJECT_VERSION = 27; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 75B6RXTKGT; @@ -1494,7 +1494,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 26; + CURRENT_PROJECT_VERSION = 27; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 75B6RXTKGT; @@ -1535,7 +1535,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 26; + CURRENT_PROJECT_VERSION = 27; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 75B6RXTKGT; @@ -1618,7 +1618,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 26; + CURRENT_PROJECT_VERSION = 27; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 75B6RXTKGT; diff --git a/ios/Ecency/Info.plist b/ios/Ecency/Info.plist index 2836b05104..85984c5a3e 100644 --- a/ios/Ecency/Info.plist +++ b/ios/Ecency/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 3.5.1 + 3.5.2 CFBundleSignature ???? CFBundleURLTypes diff --git a/ios/EcencyTests/Info.plist b/ios/EcencyTests/Info.plist index 9d06e85509..956199f661 100644 --- a/ios/EcencyTests/Info.plist +++ b/ios/EcencyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 3.5.1 + 3.5.2 CFBundleSignature ???? CFBundleVersion - 26 + 27 diff --git a/ios/eshare/Info.plist b/ios/eshare/Info.plist index 0bce586b2d..7a1e3abb09 100644 --- a/ios/eshare/Info.plist +++ b/ios/eshare/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 3.5.1 + 3.5.2 CFBundleVersion - 26 + 27 NSExtension NSExtensionAttributes diff --git a/package.json b/package.json index 55e67ac1d9..839297e49f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ecency", - "version": "3.5.1", + "version": "3.5.2", "displayName": "Ecency", "private": true, "rnpm": { @@ -35,7 +35,7 @@ "@babel/preset-typescript": "^7.26.0", "@babel/runtime": "^7.26.7", "@ecency/render-helper": "^2.4.18", - "@ecency/sdk": "^2.0.7", + "@ecency/sdk": "^2.0.9", "@esteemapp/dhive": "0.15.0", "@esteemapp/react-native-autocomplete-input": "^4.2.1", "@esteemapp/react-native-multi-slider": "^1.1.0", diff --git a/patches/hive-auth-wrapper+1.0.0.patch b/patches/hive-auth-wrapper+1.0.0.patch new file mode 100644 index 0000000000..22c57bca00 --- /dev/null +++ b/patches/hive-auth-wrapper+1.0.0.patch @@ -0,0 +1,85 @@ +diff --git a/node_modules/hive-auth-wrapper/has-wrapper.js b/node_modules/hive-auth-wrapper/has-wrapper.js +index ff0b878778..7c7906f4d6 100644 +--- a/node_modules/hive-auth-wrapper/has-wrapper.js ++++ b/node_modules/hive-auth-wrapper/has-wrapper.js +@@ -40,6 +40,7 @@ let HAS_timeout = 60*1000 // default request expiration timeout (60 seconds) + + let messages = [] + let wsHAS = undefined ++let wsHAS_creating = false // Guard against concurrent WebSocket creation + let trace = false + + function getMessage(type, uuid=undefined) { +@@ -56,9 +57,25 @@ function getMessage(type, uuid=undefined) { + + // HAS client + function startWebsocket() { +- wsHAS = new WebSocket(HAS_options.host) ++ // Guard against concurrent WebSocket creation (e.g., rapid checkConnection calls ++ // when app returns from background after Keychain interaction) ++ if(wsHAS_creating) { ++ if(trace) console.log("WebSocket creation already in progress, skipping") ++ return ++ } ++ wsHAS_creating = true ++ let ws ++ try { ++ ws = new WebSocket(HAS_options.host) ++ } catch (error) { ++ wsHAS_creating = false ++ if(trace) console.log("WebSocket creation failed", error) ++ return ++ } ++ wsHAS = ws + wsHAS.onopen = function() { + // Web Socket is connected ++ wsHAS_creating = false + HAS_connected = true + if(trace) console.log("WebSocket connected") + } +@@ -95,9 +112,14 @@ function startWebsocket() { + } + } + wsHAS.onclose = function(event) { +- // connection closed, discard old websocket +- wsHAS = undefined +- HAS_connected = false ++ wsHAS_creating = false ++ // Only clear global reference if THIS WebSocket is still the current one. ++ // Prevents a stale onclose from overwriting a newer WebSocket reference ++ // (e.g., when app returns from background and a new connection was already created). ++ if(wsHAS === ws) { ++ wsHAS = undefined ++ HAS_connected = false ++ } + if(trace) console.log("WebSocket disconnected", event) + } + } +@@ -143,19 +165,23 @@ async function attach(uuid) { + } + + async function checkConnection(uuid=undefined) { +- if ("WebSocket" in window) { +- // The browser support Websocket ++ if (typeof WebSocket !== "undefined") { + if(HAS_connected) { + return true + } +- if(!wsHAS) { ++ if(!wsHAS && !wsHAS_creating) { + startWebsocket() + } + if(!HAS_connected) { + // connection not completed yet, wait till ready ++ // Added null safety: after await sleep(), wsHAS could be undefined ++ // if onclose fired during the sleep. Also limit iterations to ++ // prevent infinite loops when WebSocket fails to connect. ++ let maxWait = Math.ceil(HAS_timeout / Math.max(1, DELAY_CHECK_WEBSOCKET)) + do { + await sleep(DELAY_CHECK_WEBSOCKET) +- } while(wsHAS && wsHAS.readyState==0) // 0 = Connecting ++ maxWait-- ++ } while(wsHAS && wsHAS.readyState === 0 && maxWait > 0) + } + if(HAS_connected && uuid) { + // WebSocket reconnected, try to attach pending request if any diff --git a/src/components/comment/view/commentView.tsx b/src/components/comment/view/commentView.tsx index ab164304c9..5563b27eb1 100644 --- a/src/components/comment/view/commentView.tsx +++ b/src/components/comment/view/commentView.tsx @@ -41,7 +41,7 @@ const CommentView = ({ handleImagePress, handleYoutubePress, handleVideoPress, - mainAuthor = { mainAuthor }, + mainAuthor = '', openReplyThread, repliesToggle, handleOnToggleReplies, @@ -229,7 +229,13 @@ const CommentView = ({ iconStyle={styles.leftIcon} style={styles.leftButton} name="delete-forever" - onPress={() => handleDeleteComment(comment.permlink, comment.parent_permlink)} + onPress={() => + handleDeleteComment( + comment.permlink, + comment.parent_permlink, + comment.parent_author, + ) + } iconType="MaterialIcons" /> )} diff --git a/src/components/comments/container/commentsContainer.tsx b/src/components/comments/container/commentsContainer.tsx index 9d16377096..6f8843866c 100644 --- a/src/components/comments/container/commentsContainer.tsx +++ b/src/components/comments/container/commentsContainer.tsx @@ -7,26 +7,22 @@ import get from 'lodash/get'; import { postBodySummary } from '@ecency/render-helper'; import { useNavigation } from '@react-navigation/native'; import { SheetManager } from 'react-native-actions-sheet'; -import { getDiscussionsQueryOptions } from '@ecency/sdk'; +import { getDiscussionsQueryOptions, useDeleteComment } from '@ecency/sdk'; import { useQueryClient } from '@tanstack/react-query'; -import { deleteComment } from '../../../providers/hive/dhive'; // Services and Actions import { writeToClipboard } from '../../../utils/clipboard'; import { toastNotification } from '../../../redux/actions/uiAction'; -// Middleware - // Constants import ROUTES from '../../../constants/routeNames'; // Component import CommentsView from '../view/commentsView'; -import { updateCommentCache } from '../../../redux/actions/cacheActions'; -import { CacheStatus } from '../../../redux/reducers/cacheReducer'; import { postQueries } from '../../../providers/queries'; import { PostTypes } from '../../../constants/postTypes'; import { SheetNames } from '../../../navigation/sheets'; -import { selectCurrentAccount, selectIsLoggedIn, selectPin } from '../../../redux/selectors'; +import { selectCurrentAccount, selectIsLoggedIn } from '../../../redux/selectors'; +import { useAuthContext } from '../../../providers/sdk'; const CommentsContainer = ({ author, @@ -35,7 +31,6 @@ const CommentsContainer = ({ isOwnProfile, fetchPost, currentAccount, - pinCode, comments, dispatch, intl, @@ -63,6 +58,8 @@ const CommentsContainer = ({ const navigation = useNavigation(); const postsCachePrimer = postQueries.usePostsCachePrimer(); const queryClient = useQueryClient(); + const authContext = useAuthContext(); + const deleteCommentMutation = useDeleteComment(currentAccount?.name, authContext); const [lcomments, setLComments] = useState([]); const [propComments, setPropComments] = useState(comments); @@ -210,8 +207,7 @@ const CommentsContainer = ({ }); }; - const _handleDeleteComment = (_permlink, _parent_permlink) => { - let filteredComments; + const _handleDeleteComment = (_permlink, _parent_permlink, _parent_author) => { if (postType === PostTypes.WAVE && handleCommentDelete) { handleCommentDelete({ _permlink, @@ -219,33 +215,23 @@ const CommentsContainer = ({ }); return; } - deleteComment(currentAccount, pinCode, _permlink).then(() => { - let deletedItem = null; - - const _applyFilter = (item) => { - if (item.permlink === _permlink) { - deletedItem = item; - return false; - } - return true; - }; - - if (lcomments.length > 0) { - filteredComments = lcomments.filter(_applyFilter); - setLComments(filteredComments); - } else { - filteredComments = propComments.filter(_applyFilter); - setPropComments(filteredComments); - } - - // remove cached entry based on parent - if (deletedItem) { - const cachePath = `${deletedItem.author}/${deletedItem.permlink}`; - deletedItem.status = CacheStatus.DELETED; - delete deletedItem.updated; - dispatch(updateCommentCache(cachePath, deletedItem, { isUpdate: true })); - } - }); + deleteCommentMutation + .mutateAsync({ + author: currentAccount?.name, + permlink: _permlink, + parentAuthor: _parent_author, + parentPermlink: _parent_permlink || permlink, + }) + .then(() => { + // Remove from local state for immediate UI update + setLComments((prev) => prev.filter((item) => item.permlink !== _permlink)); + setPropComments((prev) => prev.filter((item) => item.permlink !== _permlink)); + }) + .catch((err) => { + const errorDetail = err?.message ? String(err.message) : String(err); + dispatch(toastNotification(`Failed to delete comment: ${errorDetail}`)); + console.warn('Failed to delete comment', err); + }); }; const _handleOnUserPress = (username) => { @@ -332,7 +318,6 @@ const CommentsContainer = ({ const mapStateToProps = (state) => ({ isLoggedIn: selectIsLoggedIn(state), currentAccount: selectCurrentAccount(state), - pinCode: selectPin(state), }); export default connect(mapStateToProps)(injectIntl(CommentsContainer)); diff --git a/src/components/hiveAuthModal/hooks/useHiveAuth.ts b/src/components/hiveAuthModal/hooks/useHiveAuth.ts index 926d11e8f1..f42acffd39 100644 --- a/src/components/hiveAuthModal/hooks/useHiveAuth.ts +++ b/src/components/hiveAuthModal/hooks/useHiveAuth.ts @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { Linking, Keyboard } from 'react-native'; +import { AppState, AppStateStatus, Linking, Keyboard } from 'react-native'; import { useDispatch } from 'react-redux'; import HAS from 'hive-auth-wrapper'; @@ -214,6 +214,31 @@ const ensureHasConnection = (forceReconnect = false) => { return _hasConnectionPromise; }; +let _appStateSubscription: { remove: () => void } | null = null; +let _appStateListenerUsers = 0; +let _lastAppState: AppStateStatus = AppState.currentState; + +const ensureAppStateListener = () => { + if (_appStateSubscription) { + return; + } + + _appStateSubscription = AppState.addEventListener('change', (nextAppState) => { + if (/inactive|background/.test(_lastAppState) && nextAppState === 'active') { + console.log('[HiveAuth] App returned to foreground, forcing HAS reconnect'); + ensureHasConnection(true); + } + _lastAppState = nextAppState; + }); +}; + +const releaseAppStateListener = () => { + if (_appStateSubscription) { + _appStateSubscription.remove(); + _appStateSubscription = null; + } +}; + export const useHiveAuth = () => { const intl = useIntl(); const postLoginActions = usePostLoginActions(); @@ -230,6 +255,22 @@ export const useHiveAuth = () => { ensureHasConnection(); }, []); + // Force fresh HAS connection when app returns from background. + // When the user is redirected to Keychain for signing, the app goes to background + // and the WebSocket may disconnect. On return, we need a fresh connection before + // the broadcast's internal polling tries to use the stale one. + useEffect(() => { + if (_appStateListenerUsers++ === 0) { + ensureAppStateListener(); + } + + return () => { + if (_appStateListenerUsers > 0 && --_appStateListenerUsers === 0) { + releaseAppStateListener(); + } + }; + }, []); + /** * authenticates user via installed hive auth or keychain app * compiles and set account data in redux store diff --git a/src/components/postComments/container/postComments.tsx b/src/components/postComments/container/postComments.tsx index d07ef5fab6..b7334a64da 100644 --- a/src/components/postComments/container/postComments.tsx +++ b/src/components/postComments/container/postComments.tsx @@ -17,16 +17,12 @@ import { RefreshControl } from 'react-native-gesture-handler'; import EStyleSheet from 'react-native-extended-stylesheet'; import { SheetManager } from 'react-native-actions-sheet'; import { FlashList } from '@shopify/flash-list'; -import { useQueryClient } from '@tanstack/react-query'; -import { getDiscussionsQueryOptions } from '@ecency/sdk'; +import { useDeleteComment } from '@ecency/sdk'; import COMMENT_FILTER, { VALUE } from '../../../constants/options/comment'; import { FilterBar } from '../../filterBar'; import { postQueries } from '../../../providers/queries'; import { useAppDispatch, useAppSelector } from '../../../hooks'; import ROUTES from '../../../constants/routeNames'; -import { deleteComment } from '../../../providers/hive/dhive'; -import { updateCommentCache } from '../../../redux/actions/cacheActions'; -import { CacheStatus } from '../../../redux/reducers/cacheReducer'; import { PostTypes } from '../../../constants/postTypes'; import { CommentsSection } from '../children/commentsSection'; import { sortComments } from '../children/sortComments'; @@ -36,7 +32,9 @@ import { PostOptionsModal } from '../../index'; import { BotCommentsPreview } from '../children/botCommentsPreview'; import { SheetNames } from '../../../navigation/sheets'; import { checkViewability } from '../../../hooks/useViewabilityTracker'; -import { selectCurrentAccount, selectPin, selectIsDarkTheme } from '../../../redux/selectors'; +import { selectCurrentAccount, selectIsDarkTheme } from '../../../redux/selectors'; +import { useAuthContext } from '../../../providers/sdk'; +import { toastNotification } from '../../../redux/actions/uiAction'; const PostComments = forwardRef( ( @@ -56,14 +54,17 @@ const PostComments = forwardRef( ref, ) => { const intl = useIntl(); - const dispatch = useAppDispatch(); const navigation = useNavigation(); - const queryClient = useQueryClient(); + const dispatch = useAppDispatch(); const currentAccount = useAppSelector(selectCurrentAccount); - const pinHash = useAppSelector(selectPin); + const currentAccountName = currentAccount?.name; const isDarkTheme = useAppSelector(selectIsDarkTheme); + const authContext = useAuthContext(); + const deleteCommentMutation = useDeleteComment(currentAccountName, authContext); + const { mutateAsync: deleteComment } = deleteCommentMutation; + const discussionQuery = postQueries.useDiscussionQuery(author, permlink); const postsCachePrimer = postQueries.usePostsCachePrimer(); @@ -158,36 +159,20 @@ const PostComments = forwardRef( ); const _handleDeleteComment = useCallback( - (_permlink) => { + (_permlink, _parentPermlink?, _parentAuthor?) => { const _onConfirmDelete = async () => { try { - await deleteComment(currentAccount, pinHash, _permlink); - // remove cached entry based on parent - const _commentPath = `${currentAccount.name}/${_permlink}`; - console.log('deleted comment', _commentPath); - - const _deletedItem = discussionQuery.data?.[_commentPath]; - if (_deletedItem) { - // Don't mutate - create new object - const updatedItem = { - ..._deletedItem, - status: CacheStatus.DELETED, - }; - delete updatedItem.updated; - dispatch(updateCommentCache(_commentPath, updatedItem, { isUpdate: true })); - } - - // Invalidate discussion query cache to refresh comment list - queryClient.invalidateQueries({ - queryKey: getDiscussionsQueryOptions( - { author, permlink } as any, - 'created' as any, - true, - currentAccount.name, // Pass observer to match the query key used in useDiscussionQuery - ).queryKey, + await deleteComment({ + author: currentAccountName, + permlink: _permlink, + parentAuthor: _parentAuthor, + parentPermlink: _parentPermlink || permlink, }); + console.log('deleted comment', `${currentAccountName}/${_permlink}`); } catch (err) { - console.warn('Failed to delete comment'); + const errorDetail = err?.message ? String(err.message) : String(err); + dispatch(toastNotification(`Failed to delete comment: ${errorDetail}`)); + console.warn('Failed to delete comment', err); } }; @@ -209,16 +194,7 @@ const PostComments = forwardRef( }, }); }, - [ - currentAccount, - pinHash, - discussionQuery.data, - dispatch, - intl, - author, - permlink, - queryClient, - ], + [currentAccountName, deleteComment, dispatch, intl, permlink], ); const _openReplyThread = useCallback( diff --git a/src/components/postOptionsModal/container/postOptionsModal.tsx b/src/components/postOptionsModal/container/postOptionsModal.tsx index 31519cd395..8a4b21cc20 100644 --- a/src/components/postOptionsModal/container/postOptionsModal.tsx +++ b/src/components/postOptionsModal/container/postOptionsModal.tsx @@ -10,13 +10,9 @@ import { FlatList } from 'react-native-gesture-handler'; import ActionSheet, { SheetManager } from 'react-native-actions-sheet'; import { useQueryClient } from '@tanstack/react-query'; import { getPostQueryOptions, getAccountFullQueryOptions } from '@ecency/sdk'; -import { - deleteComment, - ignoreUser, - pinCommunityPost, - profileUpdate, - reblog, -} from '../../../providers/hive/dhive'; +import { useDeleteComment } from '@ecency/sdk'; +import { ignoreUser, pinCommunityPost, profileUpdate, reblog } from '../../../providers/hive/dhive'; +import { useAuthContext } from '../../../providers/sdk'; import { addReport } from '../../../providers/ecency/ecency'; import { toastNotification, setRcOffer } from '../../../redux/actions/uiAction'; @@ -76,6 +72,9 @@ const PostOptionsModal = ({ pageType, isWave, isVisibleTranslateModal }: Props, const isLoggedIn = useAppSelector(selectIsLoggedIn); const currentAccount = useAppSelector(selectCurrentAccount); const pinCode = useAppSelector(selectPin); + + const authContext = useAuthContext(); + const deleteCommentMutation = useDeleteComment(currentAccount?.name, authContext); const isPinCodeOpen = useAppSelector(selectIsPinCodeOpen); const subscribedCommunities = useAppSelector((state) => state.communities.subscribedCommunities); @@ -313,15 +312,31 @@ const PostOptionsModal = ({ pageType, isWave, isVisibleTranslateModal }: Props, const _deletePost = () => { const _onConfirm = async () => { - await deleteComment(currentAccount, pinCode, content.permlink); - navigation.goBack(); - dispatch( - toastNotification( - intl.formatMessage({ - id: 'alert.removed', - }), - ), - ); + try { + await deleteCommentMutation.mutateAsync({ + author: currentAccount?.name, + permlink: content.permlink, + parentAuthor: content.parent_author || '', + parentPermlink: content.parent_permlink || '', + }); + navigation.goBack(); + dispatch( + toastNotification( + intl.formatMessage({ + id: 'alert.removed', + }), + ), + ); + } catch (err) { + console.warn('Failed to delete post', err); + dispatch( + toastNotification( + intl.formatMessage({ + id: 'alert.fail', + }), + ), + ); + } }; SheetManager.show(SheetNames.ACTION_MODAL, { diff --git a/src/components/quickPostModal/usePostSubmitter.ts b/src/components/quickPostModal/usePostSubmitter.ts index 806c919123..3a1515414f 100644 --- a/src/components/quickPostModal/usePostSubmitter.ts +++ b/src/components/quickPostModal/usePostSubmitter.ts @@ -1,32 +1,30 @@ import { useDispatch } from 'react-redux'; import { Alert } from 'react-native'; import { useIntl } from 'react-intl'; -import { useQueryClient } from '@tanstack/react-query'; -import { getDiscussionsQueryOptions } from '@ecency/sdk'; +import { useComment } from '@ecency/sdk'; import { SheetManager } from 'react-native-actions-sheet'; import { useAppSelector, useStateWithRef } from '../../hooks'; -import { postComment, shouldPromptPostingAuthority } from '../../providers/hive/dhive'; +import { shouldPromptPostingAuthority } from '../../providers/hive/dhive'; import { extractMetadata, generateUniquePermlink, makeJsonMetadata } from '../../utils/editor'; -import { updateCommentCache, deleteCommentCacheEntry } from '../../redux/actions/cacheActions'; import { toastNotification } from '../../redux/actions/uiAction'; -import { useUserActivityMutation, wavesQueries } from '../../providers/queries'; -import { PointActivityIds, PollDraft } from '../../providers/ecency/ecency.types'; +import { wavesQueries } from '../../providers/queries'; +import { PollDraft } from '../../providers/ecency/ecency.types'; import { usePublishWaveMutation } from '../../providers/queries/postQueries/wavesQueries'; import { PostTypes } from '../../constants/postTypes'; import extractHashTags from '../../utils/extractHashTags'; -import { selectCurrentAccount, selectPin } from '../../redux/selectors'; +import { selectCurrentAccount } from '../../redux/selectors'; import { SheetNames } from '../../navigation/sheets'; +import { useAuthContext } from '../../providers/sdk'; export const usePostSubmitter = () => { const dispatch = useDispatch(); const intl = useIntl(); - const queryClient = useQueryClient(); const pusblishWaveMutation = usePublishWaveMutation(); const currentAccount = useAppSelector(selectCurrentAccount); - const pinCode = useAppSelector(selectPin); - const userActivityMutation = useUserActivityMutation(); + const authContext = useAuthContext(); + const commentMutation = useComment(currentAccount?.name, authContext); const [isSubmitting, setIsSubmitting, getIsSubmittingCurrent] = useStateWithRef(false); const [ _postingAuthorityPromptShown, @@ -106,7 +104,6 @@ export const usePostSubmitter = () => { const author = currentAccount.name; const parentAuthor = parentPost.author; const parentPermlink = parentPost.permlink; - const observer = currentAccount.name || currentAccount.username; const parentTags = parentPost.json_metadata.tags || ['ecency']; const category = parentPost.category || ''; const url = `/${category}/@${parentAuthor}/${parentPermlink}#@${author}/${permlink}`; @@ -123,17 +120,11 @@ export const usePostSubmitter = () => { }); const jsonMetadata = makeJsonMetadata(meta, tags); - console.log( - currentAccount, - pinCode, - parentAuthor, - parentPermlink, - permlink, - commentBody, - jsonMetadata, - ); + // Derive root author/permlink for proper cache invalidation + const rootAuthor = parentPost.root_author || parentAuthor; + const rootPermlink = parentPost.root_permlink || parentPermlink; - // Build cache entry for optimistic update + // Build cache entry for wave optimistic prepend const _cacheCommentData = { author, permlink, @@ -144,43 +135,19 @@ export const usePostSubmitter = () => { json_metadata: jsonMetadata, }; - // Optimistic: dispatch cache BEFORE blockchain call - // useDiscussionQuery's useEffect depends on cachedComments and will - // re-run injectPostCache to show the comment immediately via Redux - dispatch( - updateCommentCache(`${author}/${permlink}`, _cacheCommentData, { - parentTags: parentTags || ['ecency'], - }), - ); - try { - const response = await postComment( - currentAccount, - pinCode, + await commentMutation.mutateAsync({ + author, + permlink, parentAuthor, parentPermlink, - permlink, - commentBody, + title: '', + body: commentBody, jsonMetadata, - ); - - userActivityMutation.mutate({ - pointsTy: PointActivityIds.COMMENT, - transactionId: response.id, + rootAuthor, + rootPermlink, }); - // Invalidate discussion queries to refetch with real blockchain data - if (postType !== PostTypes.WAVE) { - queryClient.invalidateQueries({ - queryKey: getDiscussionsQueryOptions( - { author: parentAuthor, permlink: parentPermlink } as any, - 'created' as any, - true, - observer, - ).queryKey, - }); - } - dispatch( toastNotification( intl.formatMessage({ @@ -193,19 +160,6 @@ export const usePostSubmitter = () => { } catch (error) { console.log(error); - // Rollback: remove optimistic cache entry and invalidate to clean up - dispatch(deleteCommentCacheEntry(`${author}/${permlink}`)); - if (postType !== PostTypes.WAVE) { - queryClient.invalidateQueries({ - queryKey: getDiscussionsQueryOptions( - { author: parentAuthor, permlink: parentPermlink } as any, - 'created' as any, - true, - observer, - ).queryKey, - }); - } - Alert.alert( intl.formatMessage({ id: 'alert.something_wrong', diff --git a/src/providers/hive/dhive.ts b/src/providers/hive/dhive.ts index 51e6fcc6e1..d31ce9a6cd 100644 --- a/src/providers/hive/dhive.ts +++ b/src/providers/hive/dhive.ts @@ -1553,70 +1553,6 @@ export const getPurePost = async (author, permlink) => { } }; -export const deleteComment = async (currentAccount, pin, permlink) => { - const { name: author } = currentAccount; - const digitPinCode = getDigitPinCode(pin); - const key = getPostingKey(currentAccount.local, digitPinCode); - - // HiveAuth without posting authority: go directly to HiveAuth broadcast - if (shouldUseDirectHiveAuthBroadcast(currentAccount)) { - const deleteOp: Operation = ['delete_comment', { author, permlink }]; - return handleHiveAuthFallback(currentAccount, [deleteOp], 'delete_comment'); - } - - if (isHsClientSupported(currentAccount.local.authType)) { - const token = decryptKey(currentAccount.local.accessToken, digitPinCode); - const api = new hsClient({ - accessToken: token, - }); - - const params = { - author, - permlink, - }; - - const opArray = [['delete_comment', params]]; - - try { - return await api.broadcast(opArray).then((resp) => resp.result); - } catch (err) { - // Check if this is a HiveAuth user with auth error (missing authority, expired token, etc.) - const isHiveAuth = currentAccount.local?.authType === AUTH_TYPE.HIVE_AUTH; - - if (isHiveAuth && shouldTriggerHiveAuthFallback(err)) { - // Build delete_comment operation - const deleteOp: Operation = [ - 'delete_comment', - { - author, - permlink, - }, - ]; - - return handleHiveAuthFallback(currentAccount, [deleteOp], 'delete_comment'); - } - - throw err; - } - } - - if (key) { - const opArray = [ - [ - 'delete_comment', - { - author, - permlink, - }, - ], - ]; - - const privateKey = PrivateKey.fromString(key); - - return sendHiveOperations(opArray, privateKey); - } -}; - export const getDiscussionCollection = async ( author: string, permlink: string, @@ -2840,54 +2776,7 @@ export const postContent = ( }); /** - * Broadcasts a comment to post - * @param account currentAccount object - * @param pin encrypted pin taken from redux - * @param {*} parentAuthor author of parent post or in case of reply to comment author of parent comment - * @param {*} parentPermlink permlink of parent post or in case of reply to comment author of parent comment - * @param {*} permlink perlink of comment to be make - * @param {*} body body of comment - * @param {*} parentTags tags of parent post or parent comment - * @param {*} isEdit optional to avoid tracking activity in case of comment editing - * @returns - */ -export const postComment = ( - account, - pin, - parentAuthor, - parentPermlink, - permlink, - body, - jsonMetadata, -) => - _postContent( - account, - pin, - parentAuthor, - parentPermlink, - permlink, - '', - body, - jsonMetadata, - null, - null, - ) - .then((resp) => { - return resp; - }) - .catch((err) => { - console.warn('Failed to post conent', err); - captureExceptionWithRpcParams(err, { - account: account?.name, - parentAuthor, - parentPermlink, - permlink, - }); - throw err; - }); - -/** - * @method postComment post a comment/reply + * @method _postContent post content to blockchain (used for post creation/editing) * @param comment comment object { author, permlink, ... } */ const _postContent = async ( diff --git a/src/providers/queries/draftQueries.ts b/src/providers/queries/draftQueries.ts index 18ebec2546..c67920bccc 100644 --- a/src/providers/queries/draftQueries.ts +++ b/src/providers/queries/draftQueries.ts @@ -1,6 +1,7 @@ import { useMemo, useState } from 'react'; -import { useInfiniteQuery } from '@tanstack/react-query'; +import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'; import { + QueryKeys, getDraftsInfiniteQueryOptions, getSchedulesInfiniteQueryOptions, useAddDraft, @@ -14,6 +15,18 @@ import { useIntl } from 'react-intl'; import { useAppDispatch, useAuth } from '../../hooks'; import { toastNotification } from '../../redux/actions/uiAction'; +const DEFAULT_INFINITE_QUERY_LIMIT = 20; + +const draftsInfiniteQueryKey = ( + username: string | undefined, + limit = DEFAULT_INFINITE_QUERY_LIMIT, +) => QueryKeys.posts.draftsInfinite(username, limit).slice(0, 4); + +const schedulesInfiniteQueryKey = ( + username: string | undefined, + limit = DEFAULT_INFINITE_QUERY_LIMIT, +) => QueryKeys.posts.schedulesInfinite(username, limit).slice(0, 4); + /** * Hook to return user drafts with infinite scroll pagination * Uses SDK's getDraftsInfiniteQueryOptions for efficient data loading @@ -80,11 +93,14 @@ export const useAddDraftMutation = () => { const intl = useIntl(); const dispatch = useAppDispatch(); const { username, code } = useAuth(); + const queryClient = useQueryClient(); return useAddDraft( username, code, - undefined, // onSuccess - handled by SDK query invalidation + () => { + queryClient.invalidateQueries({ queryKey: draftsInfiniteQueryKey(username) }); + }, () => { dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' }))); }, @@ -99,24 +115,46 @@ export const useUpdateDraftMutation = () => { const intl = useIntl(); const dispatch = useAppDispatch(); const { username, code } = useAuth(); + const queryClient = useQueryClient(); - return useUpdateDraft(username, code, undefined, () => { - dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' }))); - }); + return useUpdateDraft( + username, + code, + () => { + queryClient.invalidateQueries({ queryKey: draftsInfiniteQueryKey(username) }); + }, + () => { + dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' }))); + }, + ); }; /** * Hook to delete a single draft * Uses SDK's useDeleteDraft hook with mobile-specific error handling + * + * NOTE: The SDK's useDeleteDraft only updates the non-infinite drafts cache key + * (["posts", "drafts", username]), but mobile uses getDraftsInfiniteQueryOptions + * which stores data under ["posts", "drafts", "infinite", username, limit]. + * We invalidate the infinite query on success so the list updates. */ export const useDraftDeleteMutation = () => { const intl = useIntl(); const dispatch = useAppDispatch(); const { username, code } = useAuth(); + const queryClient = useQueryClient(); - return useDeleteDraft(username, code, undefined, () => { - dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' }))); - }); + return useDeleteDraft( + username, + code, + () => { + // Invalidate infinite drafts query so the list re-fetches + queryClient.invalidateQueries({ queryKey: draftsInfiniteQueryKey(username) }); + }, + () => { + dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' }))); + }, + ); }; /** @@ -126,7 +164,9 @@ export const useDraftDeleteMutation = () => { export const useDraftsBatchDeleteMutation = () => { const intl = useIntl(); const dispatch = useAppDispatch(); - const deleteDraftMutation = useDraftDeleteMutation(); + const { username, code } = useAuth(); + const queryClient = useQueryClient(); + const deleteDraftMutation = useDeleteDraft(username, code); const [isBatchDeleting, setIsBatchDeleting] = useState(false); return { @@ -139,11 +179,11 @@ export const useDraftsBatchDeleteMutation = () => { // eslint-disable-next-line no-await-in-loop await deleteDraftMutation.mutateAsync({ draftId: id }); } - options?.onSettled?.(); } catch (error) { dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' }))); - options?.onSettled?.(); } finally { + await queryClient.invalidateQueries({ queryKey: draftsInfiniteQueryKey(username) }); + options?.onSettled?.(); setIsBatchDeleting(false); } }, @@ -160,12 +200,14 @@ export const useAddScheduleMutation = () => { const intl = useIntl(); const dispatch = useAppDispatch(); const { username, code } = useAuth(); + const queryClient = useQueryClient(); return useAddSchedule( username, code, () => { dispatch(toastNotification(intl.formatMessage({ id: 'alert.success' }))); + queryClient.invalidateQueries({ queryKey: schedulesInfiniteQueryKey(username) }); }, () => { dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' }))); @@ -176,15 +218,26 @@ export const useAddScheduleMutation = () => { /** * Hook to delete a single scheduled post * Uses SDK's useDeleteSchedule hook with mobile-specific error handling + * + * NOTE: Same infinite query cache mismatch as drafts - SDK updates non-infinite + * key but mobile uses getSchedulesInfiniteQueryOptions. */ export const useScheduleDeleteMutation = () => { const intl = useIntl(); const dispatch = useAppDispatch(); const { username, code } = useAuth(); + const queryClient = useQueryClient(); - return useDeleteSchedule(username, code, undefined, () => { - dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' }))); - }); + return useDeleteSchedule( + username, + code, + () => { + queryClient.invalidateQueries({ queryKey: schedulesInfiniteQueryKey(username) }); + }, + () => { + dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' }))); + }, + ); }; /** @@ -194,7 +247,9 @@ export const useScheduleDeleteMutation = () => { export const useSchedulesBatchDeleteMutation = () => { const intl = useIntl(); const dispatch = useAppDispatch(); - const deleteScheduleMutation = useScheduleDeleteMutation(); + const { username, code } = useAuth(); + const queryClient = useQueryClient(); + const deleteScheduleMutation = useDeleteSchedule(username, code); const [isBatchDeleting, setIsBatchDeleting] = useState(false); return { @@ -207,11 +262,11 @@ export const useSchedulesBatchDeleteMutation = () => { // eslint-disable-next-line no-await-in-loop await deleteScheduleMutation.mutateAsync({ id }); } - options?.onSettled?.(); } catch (error) { dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' }))); - options?.onSettled?.(); } finally { + await queryClient.invalidateQueries({ queryKey: schedulesInfiniteQueryKey(username) }); + options?.onSettled?.(); setIsBatchDeleting(false); } }, @@ -228,12 +283,16 @@ export const useMoveScheduleToDraftsMutation = () => { const intl = useIntl(); const dispatch = useAppDispatch(); const { username, code } = useAuth(); + const queryClient = useQueryClient(); return useMoveSchedule( username, code, () => { dispatch(toastNotification(intl.formatMessage({ id: 'alert.success_moved' }))); + // Invalidate both infinite queries since move affects both lists + queryClient.invalidateQueries({ queryKey: schedulesInfiniteQueryKey(username) }); + queryClient.invalidateQueries({ queryKey: draftsInfiniteQueryKey(username) }); }, () => { dispatch(toastNotification(intl.formatMessage({ id: 'alert.fail' }))); diff --git a/src/providers/queries/postQueries/commentQueries.ts b/src/providers/queries/postQueries/commentQueries.ts new file mode 100644 index 0000000000..d7bc7a96fe --- /dev/null +++ b/src/providers/queries/postQueries/commentQueries.ts @@ -0,0 +1,16 @@ +import { useComment, useUpdateReply, useDeleteComment } from '@ecency/sdk'; +import { useAuthContext } from '../../sdk'; +import { useAppSelector } from '../../../hooks'; +import { selectCurrentAccount } from '../../../redux/selectors'; + +export function useCommentMutations() { + const currentAccount = useAppSelector(selectCurrentAccount); + const authContext = useAuthContext(); + const username = currentAccount?.name; + + const commentMutation = useComment(username, authContext); + const updateReplyMutation = useUpdateReply(username, authContext); + const deleteCommentMutation = useDeleteComment(username, authContext); + + return { commentMutation, updateReplyMutation, deleteCommentMutation }; +} diff --git a/src/providers/queries/postQueries/postQueries.ts b/src/providers/queries/postQueries/postQueries.ts index 1b41e5d989..b250ef7d08 100644 --- a/src/providers/queries/postQueries/postQueries.ts +++ b/src/providers/queries/postQueries/postQueries.ts @@ -5,13 +5,7 @@ import { isArray } from 'lodash'; import { getPostQueryOptions, getDiscussionsQueryOptions, getBotsQueryOptions } from '@ecency/sdk'; import { useAppSelector } from '../../../hooks'; import { selectCurrentAccount } from '../../../redux/selectors'; -import { Comment, LastUpdateMeta } from '../../../redux/reducers/cacheReducer'; -import { - injectPostCache, - injectVoteCache, - parsePost, - parseComment, -} from '../../../utils/postParser'; +import { injectVoteCache, parsePost, parseComment } from '../../../utils/postParser'; interface PostQueryProps { author?: string; @@ -131,15 +125,11 @@ export const usePostsCachePrimer = () => { */ export const useDiscussionQuery = (_author?: string, _permlink?: string) => { const currentAccount = useAppSelector(selectCurrentAccount); - const cachedComments: { [key: string]: Comment } = useAppSelector( - (state) => state.cache.commentsCollection, - (a, b) => a === b, // Use reference equality to prevent unnecessary rerenders - ); - const cachedVotes: { [key: string]: Comment } = useAppSelector( + const cachedVotes = useAppSelector( (state) => state.cache.votesCollection, (a, b) => a === b, // Use reference equality to prevent unnecessary rerenders ); - const lastCacheUpdate: LastUpdateMeta = useAppSelector( + const lastCacheUpdate = useAppSelector( (state) => state.cache.lastUpdate, (a, b) => a === b, // Use reference equality to prevent unnecessary rerenders ); @@ -168,11 +158,7 @@ export const useDiscussionQuery = (_author?: string, _permlink?: string) => { }); useEffect(() => { - // Even if query.data is empty, we should try to show cached comments - // This handles the case where user posts a comment before discussion finishes loading - const hasCache = cachedComments && Object.keys(cachedComments).length > 0; - - if (!query.data && !hasCache) { + if (!query.data) { setData((prev) => { if (Object.keys(prev).length === 0) { return prev; @@ -182,29 +168,6 @@ export const useDiscussionQuery = (_author?: string, _permlink?: string) => { return; } - // If we have cache but no query data yet, we need to create synthetic parent entries - // so that injectPostCache can properly inject the cached comments - const initialData = {}; - if (!query.data && hasCache) { - // Create synthetic parent entries for cached comments - Object.keys(cachedComments).forEach((path) => { - const cachedComment = cachedComments[path]; - const _parentPath = `${cachedComment.parent_author}/${cachedComment.parent_permlink}`; - - // Create a minimal synthetic parent if it doesn't exist - if (!initialData[_parentPath]) { - initialData[_parentPath] = { - author: cachedComment.parent_author, - permlink: cachedComment.parent_permlink, - replies: [], - children: 0, - // Mark as synthetic so we know it's temporary - _synthetic: true, - }; - } - }); - } - // Normalize SDK response to a map keyed by "author/permlink" const normalizeReplies = (replies) => { if (!Array.isArray(replies)) { @@ -257,15 +220,14 @@ export const useDiscussionQuery = (_author?: string, _permlink?: string) => { return normalized; }; - // Use initialData if query.data is not available yet (e.g., still loading) - const normalizedData = normalizeDiscussionData(query.data || initialData); + const normalizedData = normalizeDiscussionData(query.data); // Parse SDK comments to convert markdown to HTML using render-helper // IMPORTANT: parseComment mutates its input, so we must create a shallow copy first const parsedComments = {}; Object.keys(normalizedData).forEach((key) => { const comment = normalizedData[key]; - if (comment && comment.body && !comment._synthetic) { + if (comment && comment.body) { // Create shallow copy to avoid mutating React Query cache const commentCopy = { ...comment }; parsedComments[key] = parseComment(commentCopy, currentAccount?.name); @@ -274,10 +236,24 @@ export const useDiscussionQuery = (_author?: string, _permlink?: string) => { } }); - const _data = injectPostCache(parsedComments, cachedComments, cachedVotes, lastCacheUpdate, { - author, - permlink, - }); + // Inject vote cache into parsed comments + let _data = parsedComments; + if (cachedVotes) { + let shouldClone = false; + Object.keys(_data).forEach((path) => { + const cachedVote = cachedVotes[path]; + if (cachedVote) { + const updatedComment = injectVoteCache(_data[path], cachedVote); + if (updatedComment !== _data[path]) { + if (!shouldClone) { + _data = { ..._data }; + shouldClone = true; + } + _data[path] = updatedComment; + } + } + }); + } // Deep check if data actually changed before setting state setData((prev) => { @@ -304,16 +280,7 @@ export const useDiscussionQuery = (_author?: string, _permlink?: string) => { // No changes, return previous reference return prev; }); - }, [ - query.data, - cachedComments, - cachedVotes, - lastCacheUpdate, - observer, - currentAccount?.name, - author, - permlink, - ]); + }, [query.data, cachedVotes, lastCacheUpdate, observer, currentAccount?.name, author, permlink]); // Cache to store processed comments and avoid recreating objects const processedCommentsCache = useRef>(new Map()); diff --git a/src/providers/queries/postQueries/wavesQueries.ts b/src/providers/queries/postQueries/wavesQueries.ts index 086139eb33..5d51a75beb 100644 --- a/src/providers/queries/postQueries/wavesQueries.ts +++ b/src/providers/queries/postQueries/wavesQueries.ts @@ -10,7 +10,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { unionBy, isArray } from 'lodash'; import { useDispatch } from 'react-redux'; import { useIntl } from 'react-intl'; -import { getAccountPosts, getDiscussion, useBroadcastMutation } from '@ecency/sdk'; +import { getAccountPosts, getDiscussion, useDeleteComment } from '@ecency/sdk'; import QUERIES from '../queryKeys'; import { delay } from '../../../utils/editor'; @@ -23,15 +23,8 @@ import { import { useAppSelector } from '../../../hooks'; import { toastNotification } from '../../../redux/actions/uiAction'; import { useBotAuthorsQuery } from './postQueries'; -import { - selectCurrentAccount, - selectCurrentAccountMutes, - selectPin, -} from '../../../redux/selectors'; -import authType from '../../../constants/authType'; -import { decryptKey } from '../../../utils/crypto'; -import { getDigitPinCode } from '../../hive/dhive'; -import { mapAuthTypeToLoginType } from '../../../utils/authMapper'; +import { selectCurrentAccount, selectCurrentAccountMutes } from '../../../redux/selectors'; +import { useAuthContext } from '../../sdk'; export const useWavesQuery = (host: string) => { const queryClient = useQueryClient(); @@ -116,62 +109,11 @@ export const useWavesQuery = (host: string) => { if (_timeElapsed < 5000) { if (lastCacheUpdate.type === 'vote') { _injectPostCache(lastCacheUpdate.postPath); - } else if (lastCacheUpdate.type === 'comment') { - _invalidateWaveContainerForComment(lastCacheUpdate.postPath); } } } }, [lastCacheUpdate]); - // Invalidate-only path for comment updates (no optimistic cache write here). - const _invalidateWaveContainerForComment = (commentPath: string) => { - const commentsCollection = cache.commentsCollection || {}; - const _cachedComment = commentsCollection[commentPath]; - if (!_cachedComment) { - return; - } - - const { parent_author: initialParentAuthor, parent_permlink: initialParentPermlink } = - _cachedComment; - if (!initialParentAuthor || !initialParentPermlink) { - return; - } - - // Walk ancestry until we hit a top-level wave tracked in index collection. - let parentAuthor = initialParentAuthor; - let parentPermlink = initialParentPermlink; - let parentPath = `${parentAuthor}/${parentPermlink}`; - let _containerPermlink = wavesIndexCollection.current[parentPath]; - - const visited = new Set(); - while (!_containerPermlink && parentAuthor && parentPermlink) { - if (visited.has(parentPath)) { - break; - } - visited.add(parentPath); - - const parentComment = commentsCollection[parentPath]; - if (!parentComment?.parent_author || !parentComment?.parent_permlink) { - break; - } - - parentAuthor = parentComment.parent_author; - parentPermlink = parentComment.parent_permlink; - parentPath = `${parentAuthor}/${parentPermlink}`; - _containerPermlink = wavesIndexCollection.current[parentPath]; - } - - if (_containerPermlink) { - const _containerIndex = activePermlinks.indexOf(_containerPermlink); - if (_containerIndex >= 0) { - // invalidate the specific wave container query to re-fetch with new reply - queryClient.invalidateQueries({ - queryKey: [QUERIES.WAVES.GET, host, _containerPermlink, _containerIndex], - }); - } - } - }; - const _injectPostCache = async (postPath: string) => { // using post path get index of query key where that post exists const _containerPermlink = wavesIndexCollection.current[postPath]; @@ -251,16 +193,9 @@ export const useWavesQuery = (host: string) => { ? await parseDiscussionCollection(response, currentAccount?.username) : null; - // inject cache here... - const _cachedComments = cacheRef.current.commentsCollection; + // inject vote cache here... const _cachedVotes = cacheRef.current.votesCollection; - const _lastCacheUpdate = cacheRef.current.lastCacheUpdate; - const _cResponse = injectPostCache( - parsedResponse, - _cachedComments, - _cachedVotes, - _lastCacheUpdate, - ); + const _cResponse = injectPostCache(parsedResponse, _cachedVotes); const _threadedComments = await mapDiscussionToThreads(_cResponse, host, pagePermlink, 1); @@ -396,103 +331,8 @@ export const useDeleteWaveMutation = ( const intl = useIntl(); const currentAccount = useAppSelector(selectCurrentAccount); - const pinHash = useAppSelector(selectPin); - - // Use refs to store latest values to avoid stale credentials - const currentAccountRef = useRef(currentAccount); - const pinHashRef = useRef(pinHash); - - // Update refs whenever values change - useEffect(() => { - currentAccountRef.current = currentAccount; - }, [currentAccount]); - - useEffect(() => { - pinHashRef.current = pinHash; - }, [pinHash]); - - // Compute auth credentials using refs to get fresh values - const getAuthCredentials = () => { - const account = currentAccountRef.current; - const pin = pinHashRef.current; - - // Defensive checks: verify account and required fields exist - if (!account || !account.local || !account.local.authType || !account.name) { - console.error('[WavesQueries] Missing account or auth credentials for wave deletion'); - return null; - } - - const digitPinCode = getDigitPinCode(pin); - if (!digitPinCode) { - console.error('[WavesQueries] Failed to get digit pin code'); - return null; - } - - const isHiveSigner = - account.local.authType === authType.STEEM_CONNECT || - account.local.authType === authType.HIVE_AUTH; - - const accessToken = isHiveSigner - ? decryptKey(account.local.accessToken, digitPinCode) - : undefined; - const postingKey = - !isHiveSigner && account.local.postingKey - ? decryptKey(account.local.postingKey, digitPinCode) - : undefined; - - return { - accessToken, - postingKey, - loginType: mapAuthTypeToLoginType(account.local.authType), - username: account.name, - }; - }; - - // Capture stable username for both mutation key and operation author - // to ensure they never diverge even if account changes - const usernameForKey = currentAccount.name; - - const broadcastMutation = useBroadcastMutation<{ permlink: string; parentPermlink: string }>( - [QUERIES.WAVES.DELETE], - usernameForKey, - ({ permlink }) => { - // Verify credentials at mutation time - const latestAuth = getAuthCredentials(); - if (!latestAuth) { - throw new Error('Cannot delete wave: authentication credentials are missing'); - } - // Use the same stable username for author to match mutation key - return [ - [ - 'delete_comment', - { - author: usernameForKey, - permlink, - }, - ], - ]; - }, - () => {}, // onSuccess callback - // Auth object - SDK will use this at mutation time - // Note: Cannot use IIFE here as it would capture stale credentials - // Instead, relying on SDK's internal handling of accessToken/postingKey - { - // These are computed at init time but SDK should handle refresh - // Alternative: use custom broadcast function for truly fresh credentials - get accessToken() { - const auth = getAuthCredentials(); - return auth?.accessToken; - }, - get postingKey() { - const auth = getAuthCredentials(); - return auth?.postingKey; - }, - get loginType() { - const auth = getAuthCredentials(); - return auth?.loginType || 'key'; - }, - }, - ); + const authContext = useAuthContext(); + const sdkDeleteMutation = useDeleteComment(currentAccount?.name, authContext); return useMutation({ mutationFn: async ({ @@ -502,14 +342,17 @@ export const useDeleteWaveMutation = ( _permlink: string; _parent_permlink: string; }) => { - const response = await broadcastMutation.mutateAsync({ + if (!currentAccount?.name) { + throw new Error('No authenticated user'); + } + + await sdkDeleteMutation.mutateAsync({ + author: currentAccount.name, permlink: _permlink, + parentAuthor: host, parentPermlink: _parent_permlink, }); - if (!response) { - throw new Error('Failed to delete the wave'); - } return { _permlink, _parent_permlink }; }, onSuccess: ({ _permlink, _parent_permlink }) => { diff --git a/src/redux/actions/cacheActions.ts b/src/redux/actions/cacheActions.ts index 23929da4a7..9b27a92da9 100644 --- a/src/redux/actions/cacheActions.ts +++ b/src/redux/actions/cacheActions.ts @@ -1,12 +1,7 @@ -import { renderPostBody } from '@ecency/render-helper'; -import { Platform } from 'react-native'; import { PointActivity } from '../../providers/ecency/ecency.types'; -import { makeJsonMetadataReply } from '../../utils/editor'; import { UPDATE_VOTE_CACHE, PURGE_EXPIRED_CACHE, - UPDATE_COMMENT_CACHE, - DELETE_COMMENT_CACHE_ENTRY, UPDATE_DRAFT_CACHE, DELETE_DRAFT_CACHE_ENTRY, UPDATE_REPLY_CACHE, @@ -22,16 +17,7 @@ import { UPDATE_POLL_VOTE_CACHE, UPDATE_PROPOSALS_VOTE_META, } from '../constants/constants'; -import { - Comment, - CacheStatus, - Draft, - SubscribedCommunity, - VoteCache, - PollVoteCache, -} from '../reducers/cacheReducer'; - -const COMMENT_CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes +import { Draft, SubscribedCommunity, VoteCache, PollVoteCache } from '../reducers/cacheReducer'; export const updateVoteCache = (postPath: string, vote: VoteCache) => ({ payload: { @@ -41,65 +27,6 @@ export const updateVoteCache = (postPath: string, vote: VoteCache) => ({ type: UPDATE_VOTE_CACHE, }); -interface CommentCacheOptions { - isUpdate?: boolean; - parentTags?: Array; -} - -export const updateCommentCache = ( - commentPath: string, - comment: Comment, - options: CommentCacheOptions = { isUpdate: false }, -) => { - const updated = new Date(); - updated.setSeconds(updated.getSeconds() - 5); // make cache delayed by 5 seconds to avoid same updated stamp in post data - const updatedStamp = updated.toISOString().substring(0, 19); // server only return 19 character time string without timezone part - - if (options.isUpdate && !comment.created) { - throw new Error( - 'For comment update, created prop must be provided from original comment data to update local cache', - ); - } - - if (!options.parentTags && !comment.json_metadata) { - throw new Error( - 'either of json_metadata in comment data or parentTags in options must be provided', - ); - } - - comment.created = comment.created || updatedStamp; // created will be set only once for new comment; - comment.updated = comment.updated || updatedStamp; - comment.expiresAt = comment.expiresAt || updated.getTime() + COMMENT_CACHE_TTL_MS; - comment.active_votes = comment.active_votes || []; - comment.net_rshares = comment.net_rshares || 0; - comment.author_reputation = comment.author_reputation || 25; - comment.total_payout = comment.total_payout || 0; - comment.json_metadata = comment.json_metadata || makeJsonMetadataReply(options.parentTags); - comment.children = 0; - comment.replies = []; - comment.isDeletable = comment.isDeletable || true; - comment.status = comment.status || CacheStatus.PENDING; - - comment.body = renderPostBody( - { - author: comment.author, - permlink: comment.permlink, - last_update: comment.updated, - body: comment.markdownBody, - }, - true, - Platform.OS === 'android', - ); - - return { - payload: { - commentPath, - comment, - }, - type: UPDATE_COMMENT_CACHE, - }; -}; - export const updatePollVoteCache = (postPath: string, pollVote: PollVoteCache) => ({ payload: { postPath, @@ -108,11 +35,6 @@ export const updatePollVoteCache = (postPath: string, pollVote: PollVoteCache) = type: UPDATE_POLL_VOTE_CACHE, }); -export const deleteCommentCacheEntry = (commentPath: string) => ({ - payload: commentPath, - type: DELETE_COMMENT_CACHE_ENTRY, -}); - export const updateDraftCache = (id: string, draft: Draft) => ({ payload: { id, diff --git a/src/redux/constants/constants.ts b/src/redux/constants/constants.ts index d8d8fa2643..ae8f6c33ed 100644 --- a/src/redux/constants/constants.ts +++ b/src/redux/constants/constants.ts @@ -120,8 +120,6 @@ export const SET_DEFAULT_REWARD_TYPE = 'SET_DEFAULT_REWARD_TYPE'; export const PURGE_EXPIRED_CACHE = 'PURGE_EXPIRED_CACHE'; export const UPDATE_VOTE_CACHE = 'UPDATE_VOTE_CACHE'; export const UPDATE_POLL_VOTE_CACHE = 'UPDATE_POLL_VOTE_CACHE'; -export const UPDATE_COMMENT_CACHE = 'UPDATE_COMMENT_CACHE'; -export const DELETE_COMMENT_CACHE_ENTRY = 'DELETE_COMMENT_CACHE_ENTRY'; export const UPDATE_DRAFT_CACHE = 'UPDATE_DRAFT_CACHE'; export const DELETE_DRAFT_CACHE_ENTRY = 'DELETE_DRAFT_CACHE_ENTRY'; export const UPDATE_REPLY_CACHE = 'UPDATE_REPLY_CACHE'; diff --git a/src/redux/reducers/cacheReducer.ts b/src/redux/reducers/cacheReducer.ts index ebe9e04c6c..15f3b410e3 100644 --- a/src/redux/reducers/cacheReducer.ts +++ b/src/redux/reducers/cacheReducer.ts @@ -2,8 +2,6 @@ import { PointActivity } from '../../providers/ecency/ecency.types'; import { PURGE_EXPIRED_CACHE, UPDATE_VOTE_CACHE, - UPDATE_COMMENT_CACHE, - DELETE_COMMENT_CACHE_ENTRY, DELETE_DRAFT_CACHE_ENTRY, UPDATE_DRAFT_CACHE, UPDATE_REPLY_CACHE, @@ -49,30 +47,6 @@ export interface PollVoteCache { status: CacheStatus; } -export interface Comment { - author: string; - permlink: string; - parent_author: string; - parent_permlink: string; - body?: string; - markdownBody: string; - author_reputation?: number; - total_payout?: number; - net_rshares?: number; - active_votes?: Array<{ rshares: number; voter: string }>; - replies?: string[]; - children?: number; - json_metadata?: any; - isDeletable?: boolean; - created?: string; // handle created and updated separatly - updated?: string; - expiresAt?: number; - expandedReplies?: boolean; - renderOnTop?: boolean; - status: CacheStatus; - url?: string; -} - export interface Draft { author: string; body: string; @@ -117,7 +91,6 @@ export interface LastUpdateMeta { interface State { votesCollection: { [key: string]: VoteCache }; - commentsCollection: { [key: string]: Comment }; pollVotesCollection: { [key: string]: PollVoteCache }; draftsCollection: { [key: string]: Draft }; replyCache: { [key: string]: Draft }; // For waves and reply autosave @@ -131,7 +104,6 @@ interface State { const initialState: State = { votesCollection: {}, - commentsCollection: {}, pollVotesCollection: {}, draftsCollection: {}, replyCache: {}, @@ -160,23 +132,6 @@ const cacheReducer = (state = initialState, action) => { }, }; - case UPDATE_COMMENT_CACHE: - if (!state.commentsCollection) { - state.commentsCollection = {}; - } - state.commentsCollection = { - ...state.commentsCollection, - [payload.commentPath]: payload.comment, - }; - return { - ...state, // spread operator in requried here, otherwise persist do not register change - lastUpdate: { - postPath: payload.commentPath, - updatedAt: new Date().getTime(), - type: 'comment', - }, - }; - case UPDATE_POLL_VOTE_CACHE: if (!state.pollVotesCollection) { state.pollVotesCollection = {}; @@ -194,14 +149,6 @@ const cacheReducer = (state = initialState, action) => { }, }; - case DELETE_COMMENT_CACHE_ENTRY: - if (state.commentsCollection && state.commentsCollection[payload]) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { [payload]: _removed, ...remainingComments } = state.commentsCollection; - return { ...state, commentsCollection: remainingComments }; - } - return state; - case UPDATE_DRAFT_CACHE: if (!payload.id || !payload.draft) { return state; @@ -381,15 +328,6 @@ const cacheReducer = (state = initialState, action) => { }); } - if (state.commentsCollection) { - Object.keys(state.commentsCollection).forEach((key) => { - const comment = state.commentsCollection[key]; - if (comment && (comment?.expiresAt || 0) < currentTime) { - delete state.commentsCollection[key]; - } - }); - } - if (state.pollVotesCollection) { Object.keys(state.pollVotesCollection).forEach((key) => { const vote = state.pollVotesCollection[key]; diff --git a/src/redux/selectors/index.ts b/src/redux/selectors/index.ts index bcd167c019..d3b6fe6b0d 100644 --- a/src/redux/selectors/index.ts +++ b/src/redux/selectors/index.ts @@ -217,11 +217,6 @@ export const selectVotesCollection = createSelector( export const selectCacheLastUpdate = createSelector([getCacheState], (cache) => cache.lastUpdate); -export const selectCommentsCollection = createSelector( - [getCacheState], - (cache) => cache.commentsCollection, -); - // Posts selectors export const selectFeedPosts = createSelector([getPostsState], (posts) => posts.feedPosts); diff --git a/src/screens/editor/container/editorContainer.tsx b/src/screens/editor/container/editorContainer.tsx index 7252331981..a68892b379 100644 --- a/src/screens/editor/container/editorContainer.tsx +++ b/src/screens/editor/container/editorContainer.tsx @@ -15,7 +15,6 @@ import { getDraftsInfiniteQueryOptions, getDraftsQueryOptions, getPostQueryOptions, - getDiscussionsQueryOptions, addDraft, updateDraft, addSchedule, @@ -28,7 +27,6 @@ import { postContent, grantPostingPermission, reblog, - postComment, getDigitPinCode, shouldPromptPostingAuthority, } from '../../../providers/hive/dhive'; @@ -61,8 +59,6 @@ import { DEFAULT_USER_DRAFT_ID } from '../../../redux/constants/constants'; import { deleteDraftCacheEntry, deleteReplyCacheEntry, - deleteCommentCacheEntry, - updateCommentCache, updateDraftCache, updateReplyCache, } from '../../../redux/actions/cacheActions'; @@ -70,6 +66,7 @@ import QUERIES from '../../../providers/queries/queryKeys'; import { useUserActivityMutation } from '../../../providers/queries/pointQueries'; import { PointActivityIds } from '../../../providers/ecency/ecency.types'; import { usePostsCachePrimer } from '../../../providers/queries/postQueries/postQueries'; +import { useCommentMutations } from '../../../providers/queries/postQueries/commentQueries'; import { PostTypes } from '../../../constants/postTypes'; import { @@ -1052,15 +1049,8 @@ class EditorContainer extends Component { }; _submitReply = async (fields) => { - const { - currentAccount, - pinCode, - dispatch, - userActivityMutation, - replyCache, - speakContentBuilder, - queryClient, - } = this.props; + const { currentAccount, dispatch, replyCache, speakContentBuilder, commentMutation } = + this.props; const { isPostSending } = this.state; if (isPostSending || this._isSubmitting) { @@ -1123,9 +1113,8 @@ class EditorContainer extends Component { const parentAuthor = post.author; const parentPermlink = post.permlink; - const observer = currentAccount.name || currentAccount.username; const parentTags = post.json_metadata.tags; - const draftId = `${currentAccount.name}/${parentAuthor}/${parentPermlink}`; // different draftId for each user acount + const draftId = `${currentAccount.name}/${parentAuthor}/${parentPermlink}`; const meta = await extractMetadata({ body: fields.body, @@ -1136,84 +1125,42 @@ class EditorContainer extends Component { const author = currentAccount.name; - // ✅ OPTIMISTIC UPDATE: Add comment to cache BEFORE blockchain broadcast - // This makes the comment appear instantly in the UI via Redux cache injection - // (useDiscussionQuery's useEffect depends on cachedComments and will re-run injectPostCache) - dispatch( - updateCommentCache( - `${author}/${permlink}`, - { - author, - permlink, - parent_author: parentAuthor, - parent_permlink: parentPermlink, - markdownBody: fields.body, - }, - { - parentTags: parentTags || ['ecency'], - }, - ), - ); - - // Broadcast to blockchain - await postComment( - currentAccount, - pinCode, - parentAuthor, - parentPermlink, - permlink, - fields.body, - jsonMetadata, - ) - .then((response) => { - // record user activity for points - userActivityMutation.mutate({ - pointsTy: PointActivityIds.COMMENT, - transactionId: response.id, - }); + // Derive root author/permlink for proper cache invalidation + const rootAuthor = post.root_author || parentAuthor; + const rootPermlink = post.root_permlink || parentPermlink; - AsyncStorage.setItem('temp-reply', ''); - this._handleSubmitSuccess(); - - // Invalidate discussion queries to refetch with real blockchain data - queryClient.invalidateQueries({ - queryKey: getDiscussionsQueryOptions( - { author: parentAuthor, permlink: parentPermlink } as any, - 'created' as any, - true, - observer, - ).queryKey, - }); + try { + await commentMutation.mutateAsync({ + author, + permlink, + parentAuthor, + parentPermlink, + title: '', + body: fields.body, + jsonMetadata, + rootAuthor, + rootPermlink, + }); - // delete quick comment draft cache if it exist (from replyCache) - if (replyCache && replyCache[draftId]) { - dispatch(deleteReplyCacheEntry(draftId)); - } + AsyncStorage.setItem('temp-reply', ''); + this._handleSubmitSuccess(); - this._isSubmitting = false; - }) - .catch((error) => { - // ❌ ROLLBACK: Remove optimistic comment on blockchain failure - dispatch(deleteCommentCacheEntry(`${author}/${permlink}`)); - - // Invalidate discussion queries to clean up - queryClient.invalidateQueries({ - queryKey: getDiscussionsQueryOptions( - { author: parentAuthor, permlink: parentPermlink } as any, - 'created' as any, - true, - observer, - ).queryKey, - }); + // delete quick comment draft cache if it exist (from replyCache) + if (replyCache && replyCache[draftId]) { + dispatch(deleteReplyCacheEntry(draftId)); + } - this._isSubmitting = false; - this._handleSubmitFailure(error); - }); + this._isSubmitting = false; + } catch (error) { + this._isSubmitting = false; + this._handleSubmitFailure(error); + } } }; _submitEdit = async (fields) => { - const { currentAccount, pinCode, dispatch, postCachePrimer, speakContentBuilder } = this.props; + const { currentAccount, pinCode, postCachePrimer, speakContentBuilder, updateReplyMutation } = + this.props; const { post, isEdit, isPostSending, thumbUrl, isReply } = this.state; if (isPostSending) { @@ -1298,60 +1245,66 @@ class EditorContainer extends Component { jsonMeta = makeJsonMetadata(meta, tags); } - await postContent( - currentAccount, - pinCode, - parentAuthor || '', - parentPermlink || '', - permlink, - title || '', - newBody, - jsonMeta, - null, - null, - isEdit, - ) - .then(() => { + try { + if (isReply) { + // Use SDK updateReplyMutation for reply edits const author = currentAccount.name; + const rootAuthor = post.root_author || parentAuthor; + const rootPermlink = post.root_permlink || parentPermlink; + + await updateReplyMutation.mutateAsync({ + author, + permlink, + parentAuthor: parentAuthor || '', + parentPermlink: parentPermlink || '', + title: '', + body: newBody, + jsonMetadata: jsonMeta, + rootAuthor, + rootPermlink, + }); + + // Update local cache for immediate UI feedback + postCachePrimer.cachePost({ + ...post, + body, + json_metadata: jsonMeta, + markdownBody: body, + updated: new Date().toISOString(), + }); + + AsyncStorage.setItem('temp-reply', ''); this._handleSubmitSuccess(); - if (isReply) { - AsyncStorage.setItem('temp-reply', ''); - dispatch( - updateCommentCache( - `${author}/${permlink}`, - { - author, - permlink, - parent_author: parentAuthor, - parent_permlink: parentPermlink, - markdownBody: body, - active_votes: post.active_votes, - net_rshares: post.net_rshares, - author_reputation: post.author_reputation, - total_payout: post.total_payout, - created: post.created, - json_metadata: jsonMeta, - }, - { - isUpdate: true, - }, - ), - ); - } else { - // update post query data - postCachePrimer.cachePost({ - ...post, - title, - body, - json_metadata: jsonMeta, - markdownBody: body, - updated: new Date().toISOString(), - }); - } - }) - .catch((error) => { - this._handleSubmitFailure(error); - }); + } else { + // Use postContent for post edits (non-reply) + await postContent( + currentAccount, + pinCode, + parentAuthor || '', + parentPermlink || '', + permlink, + title || '', + newBody, + jsonMeta, + null, + null, + isEdit, + ); + + this._handleSubmitSuccess(); + // update post query data + postCachePrimer.cachePost({ + ...post, + title, + body, + json_metadata: jsonMeta, + markdownBody: body, + updated: new Date().toISOString(), + }); + } + } catch (error) { + this._handleSubmitFailure(error); + } } }; @@ -1723,6 +1676,7 @@ const mapQueriesToProps = () => ({ speakMutations: speakQueries.useSpeakMutations(), userActivityMutation: useUserActivityMutation(), postCachePrimer: usePostsCachePrimer(), + ...useCommentMutations(), }); export default gestureHandlerRootHOC( diff --git a/src/utils/postParser.tsx b/src/utils/postParser.tsx index ef84385e1c..7d77bc70e1 100644 --- a/src/utils/postParser.tsx +++ b/src/utils/postParser.tsx @@ -272,16 +272,10 @@ export const parseComment = (comment: any, currentUsername?: string, currentTime return comment; }; -export const injectPostCache = ( - commentsMap, - cachedComments, - cachedVotes, - lastCacheUpdate, - discussionContext?: { author?: string; permlink?: string }, -) => { +export const injectPostCache = (commentsMap, cachedVotes) => { let shouldClone = false; let _comments = commentsMap || {}; - if (!cachedComments && !cachedVotes) { + if (!cachedVotes) { return _comments; } @@ -289,140 +283,23 @@ export const injectPostCache = ( return _comments; } - // process comments cache - if (cachedComments) { - Object.keys(cachedComments).forEach((path) => { - const currentTime = new Date().getTime(); - const cachedComment = cachedComments[path]; - const _parentPath = `${cachedComment.parent_author}/${cachedComment.parent_permlink}`; - const cacheUpdateTimestamp = new Date(cachedComment.updated || 0).getTime(); - - switch (cachedComment.status) { - case CacheStatus.DELETED: - if (_comments && _comments[path]) { - if (!shouldClone) { - _comments = { ..._comments }; - shouldClone = true; - } - delete _comments[path]; - - // Update parent's children count and replies array - if (_comments[_parentPath]) { - _comments[_parentPath] = { - ..._comments[_parentPath], - children: Math.max(0, (_comments[_parentPath].children ?? 1) - 1), - replies: (_comments[_parentPath].replies || []).filter((r: string) => r !== path), - }; - } - } - break; - case CacheStatus.UPDATED: - case CacheStatus.PENDING: - // check if commentKey already exist in comments map, - if (_comments[path]) { - // check if we should update comments map with cached map based on updat timestamp - const remoteUpdateTimestamp = new Date(_comments[path].updated).getTime(); - - if (cacheUpdateTimestamp > remoteUpdateTimestamp) { - if (!shouldClone) { - _comments = { ..._comments }; - shouldClone = true; - } - // Don't mutate - create new object - _comments[path] = { - ..._comments[path], - body: cachedComment.body, - }; - } - } - - // if comment key do not exist, possibly comment is a new comment, in this case, check if parent of comment exist in map - else if (_comments[_parentPath]) { - if (!shouldClone) { - _comments = { ..._comments }; - shouldClone = true; - } - // in this case add comment key in children and inject cachedComment in commentsMap - const parentComment = _comments[_parentPath]; - let updatedComment = cachedComment; - if (cachedVotes && cachedVotes[path]) { - updatedComment = injectVoteCache(updatedComment, cachedVotes[path]); - } - _comments[path] = updatedComment; - - // Don't mutate parent - create new object with updated replies and children - _comments[_parentPath] = { - ...parentComment, - replies: [...(parentComment.replies || []), path], - children: (parentComment.children ?? 0) + 1, - }; - - // if comment was created very recently enable auto reveal - if ( - lastCacheUpdate && - lastCacheUpdate.postPath === path && - currentTime - lastCacheUpdate.updatedAt < 5000 - ) { - _comments[_parentPath] = { - ..._comments[_parentPath], - expandedReplies: true, - }; - _comments[path] = { - ..._comments[path], - renderOnTop: true, - }; - } - } - // fallback: parent entry might be excluded from fetched discussion map - // keep root-level optimistic comments visible for the currently opened discussion - else if ( - discussionContext?.author && - discussionContext?.permlink && - cachedComment.parent_author === discussionContext.author && - cachedComment.parent_permlink === discussionContext.permlink - ) { - if (!shouldClone) { - _comments = { ..._comments }; - shouldClone = true; - } - - let updatedComment = cachedComment; - if (cachedVotes && cachedVotes[path]) { - updatedComment = injectVoteCache(updatedComment, cachedVotes[path]); - } - if (lastCacheUpdate && currentTime - lastCacheUpdate.updatedAt < 5000) { - _comments[path] = { - ...updatedComment, - renderOnTop: true, - }; - } else { - _comments[path] = updatedComment; - } - } - break; - } - }); - } - const commentPaths = Object.keys(_comments); - // process votes cache - only for comments in this discussion (after cached inserts) - if (cachedVotes) { - commentPaths.forEach((path) => { - const cachedVote = cachedVotes[path]; - if (cachedVote) { - const updatedComment = injectVoteCache(_comments[path], cachedVote); - // Only update if injectVoteCache returned a new reference (meaning cache was applied) - if (updatedComment !== _comments[path]) { - if (!shouldClone) { - _comments = { ..._comments }; - shouldClone = true; - } - _comments[path] = updatedComment; + // process votes cache - only for comments in this discussion + commentPaths.forEach((path) => { + const cachedVote = cachedVotes[path]; + if (cachedVote) { + const updatedComment = injectVoteCache(_comments[path], cachedVote); + // Only update if injectVoteCache returned a new reference (meaning cache was applied) + if (updatedComment !== _comments[path]) { + if (!shouldClone) { + _comments = { ..._comments }; + shouldClone = true; } + _comments[path] = updatedComment; } - }); - } + } + }); return _comments; }; diff --git a/yarn.lock b/yarn.lock index caad49972f..81c9b9f3bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1180,10 +1180,10 @@ url "^0.11.0" xss "^1.0.9" -"@ecency/sdk@^2.0.7": - version "2.0.7" - resolved "https://registry.yarnpkg.com/@ecency/sdk/-/sdk-2.0.7.tgz#77aef645fd1f7e708c52a61086303df14786d219" - integrity sha512-YUEHOHB8vIfQJmLQjrXjswPhfNGx6XKyduAu6QMBekw3GS/8yuI9MHlFi8y+bm210VRqS4Wi+BmOjHrA7Toufw== +"@ecency/sdk@^2.0.9": + version "2.0.9" + resolved "https://registry.yarnpkg.com/@ecency/sdk/-/sdk-2.0.9.tgz#cd84d8bf2dbe192134eb712c9a3b145156fa0cef" + integrity sha512-JJatN+jQVZGzO9agUkn8lqeGVH00VZ3eSOEwpIsMaLZWWaL3HOgfzcm2CLrKvZdhZJyoMEJFDToZIQ5WPxPe2w== "@egjs/hammerjs@^2.0.17": version "2.0.17"