Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions ios/Ecency.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion ios/Ecency/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>3.5.1</string>
<string>3.5.2</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
Expand Down
4 changes: 2 additions & 2 deletions ios/EcencyTests/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>3.5.1</string>
<string>3.5.2</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>26</string>
<string>27</string>
</dict>
</plist>
4 changes: 2 additions & 2 deletions ios/eshare/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>3.5.1</string>
<string>3.5.2</string>
<key>CFBundleVersion</key>
<string>26</string>
<string>27</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ecency",
"version": "3.5.1",
"version": "3.5.2",
"displayName": "Ecency",
"private": true,
"rnpm": {
Expand Down Expand Up @@ -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",
Expand Down
85 changes: 85 additions & 0 deletions patches/hive-auth-wrapper+1.0.0.patch
Original file line number Diff line number Diff line change
@@ -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
10 changes: 8 additions & 2 deletions src/components/comment/view/commentView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const CommentView = ({
handleImagePress,
handleYoutubePress,
handleVideoPress,
mainAuthor = { mainAuthor },
mainAuthor = '',
openReplyThread,
repliesToggle,
handleOnToggleReplies,
Expand Down Expand Up @@ -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"
/>
)}
Expand Down
61 changes: 23 additions & 38 deletions src/components/comments/container/commentsContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -35,7 +31,6 @@ const CommentsContainer = ({
isOwnProfile,
fetchPost,
currentAccount,
pinCode,
comments,
dispatch,
intl,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -210,42 +207,31 @@ const CommentsContainer = ({
});
};

const _handleDeleteComment = (_permlink, _parent_permlink) => {
let filteredComments;
const _handleDeleteComment = (_permlink, _parent_permlink, _parent_author) => {
if (postType === PostTypes.WAVE && handleCommentDelete) {
handleCommentDelete({
_permlink,
_parent_permlink,
});
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) => {
Expand Down Expand Up @@ -332,7 +318,6 @@ const CommentsContainer = ({
const mapStateToProps = (state) => ({
isLoggedIn: selectIsLoggedIn(state),
currentAccount: selectCurrentAccount(state),
pinCode: selectPin(state),
});

export default connect(mapStateToProps)(injectIntl(CommentsContainer));
43 changes: 42 additions & 1 deletion src/components/hiveAuthModal/hooks/useHiveAuth.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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();
Expand All @@ -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
Expand Down
Loading