fix: a few issues with group invite job/refresh state

pull/3052/head
Audric Ackermann 1 year ago
parent 95cd940948
commit 9963287193

@ -61,7 +61,7 @@
"sedtoAppImage": "sed -i 's/\"target\": \\[\"deb\", \"rpm\", \"freebsd\"\\]/\"target\": \"AppImage\"/g' package.json", "sedtoAppImage": "sed -i 's/\"target\": \\[\"deb\", \"rpm\", \"freebsd\"\\]/\"target\": \"AppImage\"/g' package.json",
"sedtoDeb": "sed -i 's/\"target\": \"AppImage\"/\"target\": \\[\"deb\", \"rpm\", \"freebsd\"\\]/g' package.json", "sedtoDeb": "sed -i 's/\"target\": \"AppImage\"/\"target\": \\[\"deb\", \"rpm\", \"freebsd\"\\]/g' package.json",
"ready": "yarn build-everything && yarn lint-full && yarn test", "ready": "yarn build-everything && yarn lint-full && yarn test",
"postinstall": "yarn patch-package", "postinstall": "yarn patch-package && yarn electron-builder install-app-deps",
"update-git-info": "node ./build/updateLocalConfig.js", "update-git-info": "node ./build/updateLocalConfig.js",
"worker:utils": "webpack --config=./utils.worker.config.js", "worker:utils": "webpack --config=./utils.worker.config.js",
"worker:libsession": "rimraf 'ts/webworker/workers/node/libsession/*.node' && webpack --config=./libsession.worker.config.js", "worker:libsession": "rimraf 'ts/webworker/workers/node/libsession/*.node' && webpack --config=./libsession.worker.config.js",
@ -95,7 +95,7 @@
"fs-extra": "9.0.0", "fs-extra": "9.0.0",
"glob": "10.3.10", "glob": "10.3.10",
"image-type": "^4.1.0", "image-type": "^4.1.0",
"libsession_util_nodejs": "link:../libsession-util-nodejs", "libsession_util_nodejs": "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.3.19/libsession_util_nodejs-v0.3.19.tar.gz",
"libsodium-wrappers-sumo": "^0.7.9", "libsodium-wrappers-sumo": "^0.7.9",
"linkify-it": "^4.0.1", "linkify-it": "^4.0.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",

@ -32,4 +32,4 @@ index da53a62..4db8b56 100644
+ id: string; + id: string;
idAttribute: string; idAttribute: string;
validationError: any; validationError: any;

@ -145,14 +145,18 @@ const GroupStatusText = ({ groupPk, pubkey }: { pubkey: PubkeyType; groupPk: Gro
const groupPromotionSent = useMemberPromotionSent(pubkey, groupPk); const groupPromotionSent = useMemberPromotionSent(pubkey, groupPk);
const groupInviteSending = useMemberInviteSending(groupPk, pubkey); const groupInviteSending = useMemberInviteSending(groupPk, pubkey);
const statusText = groupPromotionFailed /**
? window.i18n('promotionFailed') * Note: Keep the "sending" checks here first, as we might be "sending" when we've previously failed.
: groupInviteFailed * If we were to have the "failed" checks first, we'd hide the "sending" state when we are retrying.
? window.i18n('inviteFailed') */
: groupInviteSending const statusText = groupInviteSending
? window.i18n('inviteSending') ? window.i18n('inviteSending')
: groupPromotionSending : groupPromotionSending
? window.i18n('promotionSending') ? window.i18n('promotionSending')
: groupPromotionFailed
? window.i18n('promotionFailed')
: groupInviteFailed
? window.i18n('inviteFailed')
: groupInviteSent : groupInviteSent
? window.i18n('inviteSent') ? window.i18n('inviteSent')
: groupPromotionSent : groupPromotionSent
@ -165,7 +169,7 @@ const GroupStatusText = ({ groupPk, pubkey }: { pubkey: PubkeyType; groupPk: Gro
return ( return (
<StyledGroupStatusText <StyledGroupStatusText
data-testid={'group-member-status-text'} data-testid={'group-member-status-text'}
isFailure={groupPromotionFailed || groupInviteFailed} isFailure={(groupPromotionFailed && !groupPromotionSending) || (groupInviteFailed && !groupInviteSending)}
> >
{statusText} {statusText}
</StyledGroupStatusText> </StyledGroupStatusText>
@ -196,6 +200,10 @@ const ResendInviteButton = ({
pubkey: PubkeyType; pubkey: PubkeyType;
groupPk: GroupPubkeyType; groupPk: GroupPubkeyType;
}) => { }) => {
const inviteFailed = useMemberInviteFailed(pubkey, groupPk);
if (!inviteFailed) {
return null;
}
return ( return (
<SessionButton <SessionButton
dataTestId={'resend-invite-button'} dataTestId={'resend-invite-button'}

@ -11,7 +11,7 @@ import { updateGroupMembersModal } from '../../state/ducks/modalDialog';
import { MemberListItem } from '../MemberListItem'; import { MemberListItem } from '../MemberListItem';
import { SessionWrapperModal } from '../SessionWrapperModal'; import { SessionWrapperModal } from '../SessionWrapperModal';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton'; import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
import { SpacerLG, Text } from '../basic/Text'; import { SpacerLG } from '../basic/Text';
import { import {
useConversationUsername, useConversationUsername,
@ -20,7 +20,6 @@ import {
useIsPublic, useIsPublic,
useSortedGroupMembers, useSortedGroupMembers,
useWeAreAdmin, useWeAreAdmin,
useZombies,
} from '../../hooks/useParamSelector'; } from '../../hooks/useParamSelector';
import { useSet } from '../../hooks/useSet'; import { useSet } from '../../hooks/useSet';
@ -88,49 +87,6 @@ const ClassicMemberList = (props: {
); );
}; };
const ZombiesList = ({ convoId }: { convoId: string }) => {
const weAreAdmin = useWeAreAdmin(convoId);
const zombies = useZombies(convoId);
function onZombieClicked() {
if (!weAreAdmin) {
ToastUtils.pushOnlyAdminCanRemove();
}
}
if (!zombies?.length) {
return null;
}
const zombieElements = zombies.map((zombie: string) => {
const isSelected = weAreAdmin || false; // && !member.checkmarked;
return (
<MemberListItem
isSelected={isSelected}
onSelect={onZombieClicked}
onUnselect={onZombieClicked}
isZombie={true}
key={zombie}
pubkey={zombie}
/>
);
});
return (
<>
<SpacerLG />
{weAreAdmin && (
<Text
padding="20px"
text={window.i18n('removeResidueMembers')}
subtle={true}
maxWidth="400px"
textAlign="center"
/>
)}
{zombieElements}
</>
);
};
async function onSubmit(convoId: string, membersAfterUpdate: Array<string>) { async function onSubmit(convoId: string, membersAfterUpdate: Array<string>) {
const convoFound = ConvoHub.use().get(convoId); const convoFound = ConvoHub.use().get(convoId);
if (!convoFound || !convoFound.isGroup()) { if (!convoFound || !convoFound.isGroup()) {
@ -275,7 +231,7 @@ export const UpdateGroupMembersDialog = (props: Props) => {
return ( return (
<SessionWrapperModal title={titleText} onClose={closeDialog}> <SessionWrapperModal title={titleText} onClose={closeDialog}>
{hasClosedGroupV2QAButtons() ? ( {hasClosedGroupV2QAButtons() && weAreAdmin ? (
<> <>
Also remove messages: Also remove messages:
<SessionToggle <SessionToggle
@ -294,7 +250,6 @@ export const UpdateGroupMembersDialog = (props: Props) => {
selectedMembers={membersToKeepWithUpdate} selectedMembers={membersToKeepWithUpdate}
/> />
</StyledClassicMemberList> </StyledClassicMemberList>
<ZombiesList convoId={conversationId} />
{showNoMembersMessage && <p>{window.i18n('noMembersInThisGroup')}</p>} {showNoMembersMessage && <p>{window.i18n('noMembersInThisGroup')}</p>}
<SpacerLG /> <SpacerLG />

@ -582,11 +582,13 @@ async function handleLegacyGroupUpdate(latestEnvelopeTimestamp: number) {
toLeave.id toLeave.id
); );
const toLeaveFromDb = ConvoHub.use().get(toLeave.id); const toLeaveFromDb = ConvoHub.use().get(toLeave.id);
// the wrapper told us that this group is not tracked, so even if we left/got kicked from it, remove it from the DB completely if (PubKey.is05Pubkey(toLeaveFromDb.id)) {
await ConvoHub.use().deleteLegacyGroup(toLeaveFromDb.id, { // the wrapper told us that this group is not tracked, so even if we left/got kicked from it, remove it from the DB completely
fromSyncMessage: true, await ConvoHub.use().deleteLegacyGroup(toLeaveFromDb.id, {
sendLeaveMessage: false, // this comes from the wrapper, so we must have left/got kicked from that group already and our device already handled it. fromSyncMessage: true,
}); sendLeaveMessage: false, // this comes from the wrapper, so we must have left/got kicked from that group already and our device already handled it.
});
}
} }
for (let index = 0; index < legacyGroupsToJoinInDB.length; index++) { for (let index = 0; index < legacyGroupsToJoinInDB.length; index++) {

@ -1135,10 +1135,7 @@ function getPathString(pathObjArr: Array<{ ip: string; port: number }>): string
return pathObjArr.map(node => `${node.ip}:${node.port}`).join(', '); return pathObjArr.map(node => `${node.ip}:${node.port}`).join(', ');
} }
/** async function lokiOnionFetchNoRetries({
* If the fetch throws a retryable error we retry this call with a new path at most 3 times. If another error happens, we return it. If we have a result we just return it.
*/
async function lokiOnionFetchWithRetries({
targetNode, targetNode,
associatedWith, associatedWith,
body, body,
@ -1152,33 +1149,17 @@ async function lokiOnionFetchWithRetries({
allow401s: boolean; allow401s: boolean;
}): Promise<SnodeResponse | undefined> { }): Promise<SnodeResponse | undefined> {
try { try {
const retriedResult = await pRetry( // Get a path excluding `targetNode`:
async () => { const path = await OnionPaths.getOnionPath({ toExclude: targetNode });
// Get a path excluding `targetNode`: const result = await sendOnionRequestSnodeDestNoRetries(
const path = await OnionPaths.getOnionPath({ toExclude: targetNode }); path,
const result = await sendOnionRequestSnodeDestNoRetries( targetNode,
path, headers,
targetNode, body,
headers, allow401s,
body, associatedWith
allow401s,
associatedWith
);
return result;
},
{
retries: 3,
factor: 1,
minTimeout: 100,
onFailedAttempt: e => {
window?.log?.warn(
`onionFetchRetryable attempt #${e.attemptNumber} failed. ${e.retriesLeft} retries left...`
);
},
}
); );
return result as SnodeResponse | undefined;
return retriedResult as SnodeResponse | undefined;
} catch (e) { } catch (e) {
window?.log?.warn('onionFetchRetryable failed ', e.message); window?.log?.warn('onionFetchRetryable failed ', e.message);
if (e?.errno === 'ENETUNREACH') { if (e?.errno === 'ENETUNREACH') {
@ -1197,7 +1178,7 @@ export const Onions = {
sendOnionRequestHandlingSnodeEjectNoRetries, sendOnionRequestHandlingSnodeEjectNoRetries,
incrementBadSnodeCountOrDrop, incrementBadSnodeCountOrDrop,
decodeOnionResult, decodeOnionResult,
lokiOnionFetchWithRetries, lokiOnionFetchNoRetries,
getPathString, getPathString,
sendOnionRequestSnodeDestNoRetries, sendOnionRequestSnodeDestNoRetries,
processOnionResponse, processOnionResponse,

@ -54,7 +54,7 @@ async function doRequestNoRetries({
? true ? true
: window.sessionFeatureFlags?.useOnionRequests; : window.sessionFeatureFlags?.useOnionRequests;
if (useOnionRequests && targetNode) { if (useOnionRequests && targetNode) {
const fetchResult = await Onions.lokiOnionFetchWithRetries({ const fetchResult = await Onions.lokiOnionFetchNoRetries({
targetNode, targetNode,
body: fetchOptions.body, body: fetchOptions.body,
headers: fetchOptions.headers, headers: fetchOptions.headers,

@ -11,11 +11,18 @@ export function pushToastError(id: string, description: string) {
}); });
} }
export function pushToastWarning(id: string, description: string) { export function pushToastWarning(id: string, description: string, onToastClick?: () => void) {
toast.warning(<SessionToast description={description} type={SessionToastType.Warning} />, { toast.warning(
toastId: id, <SessionToast
updateId: id, description={description}
}); type={SessionToastType.Warning}
onToastClick={onToastClick}
/>,
{
toastId: id,
updateId: id,
}
);
} }
export function pushToastInfo( export function pushToastInfo(

@ -18,6 +18,8 @@ import {
PersistedJob, PersistedJob,
RunJobResult, RunJobResult,
} from '../PersistedJob'; } from '../PersistedJob';
import { LibSessionUtil } from '../../libsession/libsession_utils';
import { showUpdateGroupMembersByConvoId } from '../../../../interactions/conversationInteractions';
const defaultMsBetweenRetries = 10000; const defaultMsBetweenRetries = 10000;
const defaultMaxAttemps = 1; const defaultMaxAttemps = 1;
@ -51,15 +53,9 @@ async function addJob({ groupPk, member }: JobExtraArgs) {
nextAttemptTimestamp: Date.now(), nextAttemptTimestamp: Date.now(),
}); });
window.log.debug(`addGroupInviteJob: adding group invite for ${groupPk}:${member} `); window.log.debug(`addGroupInviteJob: adding group invite for ${groupPk}:${member} `);
try {
updateFailedStateForMember(groupPk, member, false);
// we have to reset the error state so that the UI shows "sending" even if the last try failed.
await MetaGroupWrapperActions.memberSetInvited(groupPk, member, false);
} catch (e) {
window.log.warn('GroupInviteJob memberSetInvited (before) failed with', e.message);
}
window?.inboxStore?.dispatch(groupInfoActions.refreshGroupDetailsFromWrapper({ groupPk })); window?.inboxStore?.dispatch(groupInfoActions.refreshGroupDetailsFromWrapper({ groupPk }));
await LibSessionUtil.saveDumpsToDb(groupPk);
await runners.groupInviteJobRunner.addJob(groupInviteJob); await runners.groupInviteJobRunner.addJob(groupInviteJob);
@ -71,21 +67,27 @@ async function addJob({ groupPk, member }: JobExtraArgs) {
function displayFailedInvitesForGroup(groupPk: GroupPubkeyType) { function displayFailedInvitesForGroup(groupPk: GroupPubkeyType) {
const thisGroupFailures = invitesFailed.get(groupPk); const thisGroupFailures = invitesFailed.get(groupPk);
if (!thisGroupFailures || thisGroupFailures.failedMembers.length === 0) { if (!thisGroupFailures || thisGroupFailures.failedMembers.length === 0) {
return; return;
} }
const onToastClick = () => {
void showUpdateGroupMembersByConvoId(groupPk);
};
const count = thisGroupFailures.failedMembers.length; const count = thisGroupFailures.failedMembers.length;
switch (count) { switch (count) {
case 1: case 1:
ToastUtils.pushToastWarning( ToastUtils.pushToastWarning(
`invite-failed${groupPk}`, `invite-failed${groupPk}`,
window.i18n('groupInviteFailedOne', [...thisGroupFailures.failedMembers, groupPk]) window.i18n('groupInviteFailedOne', [...thisGroupFailures.failedMembers, groupPk]),
onToastClick
); );
break; break;
case 2: case 2:
ToastUtils.pushToastWarning( ToastUtils.pushToastWarning(
`invite-failed${groupPk}`, `invite-failed${groupPk}`,
window.i18n('groupInviteFailedTwo', [...thisGroupFailures.failedMembers, groupPk]) window.i18n('groupInviteFailedTwo', [...thisGroupFailures.failedMembers, groupPk]),
onToastClick
); );
break; break;
default: default:
@ -95,7 +97,8 @@ function displayFailedInvitesForGroup(groupPk: GroupPubkeyType) {
thisGroupFailures.failedMembers[0], thisGroupFailures.failedMembers[0],
`${thisGroupFailures.failedMembers.length - 1}`, `${thisGroupFailures.failedMembers.length - 1}`,
groupPk, groupPk,
]) ]),
onToastClick
); );
} }
// toast was displayed empty the list // toast was displayed empty the list
@ -169,7 +172,11 @@ class GroupInviteJob extends PersistedJob<GroupInvitePersistedData> {
window.log.warn( window.log.warn(
`${jobType} with groupPk:"${groupPk}" member: ${member} id:"${identifier}" failed with ${e.message}` `${jobType} with groupPk:"${groupPk}" member: ${member} id:"${identifier}" failed with ${e.message}`
); );
failed = true;
} finally { } finally {
window.log.info(
`${jobType} with groupPk:"${groupPk}" member: ${member} id:"${identifier}" finished. failed:${failed}`
);
try { try {
await MetaGroupWrapperActions.memberSetInvited(groupPk, member, failed); await MetaGroupWrapperActions.memberSetInvited(groupPk, member, failed);
} catch (e) { } catch (e) {
@ -177,7 +184,11 @@ class GroupInviteJob extends PersistedJob<GroupInvitePersistedData> {
} }
updateFailedStateForMember(groupPk, member, failed); updateFailedStateForMember(groupPk, member, failed);
window?.inboxStore?.dispatch(
groupInfoActions.setInvitePending({ groupPk, pubkey: member, sending: false })
);
window?.inboxStore?.dispatch(groupInfoActions.refreshGroupDetailsFromWrapper({ groupPk })); window?.inboxStore?.dispatch(groupInfoActions.refreshGroupDetailsFromWrapper({ groupPk }));
await LibSessionUtil.saveDumpsToDb(groupPk);
} }
// return true so this job is marked as a success and we don't need to retry it // return true so this job is marked as a success and we don't need to retry it
return RunJobResult.Success; return RunJobResult.Success;

@ -13,6 +13,8 @@ import {
DeleteAllFromGroupMsgNodeSubRequest, DeleteAllFromGroupMsgNodeSubRequest,
StoreGroupKeysSubRequest, StoreGroupKeysSubRequest,
StoreGroupMessageSubRequest, StoreGroupMessageSubRequest,
SubaccountRevokeSubRequest,
SubaccountUnrevokeSubRequest,
} from '../../../apis/snode_api/SnodeRequestTypes'; } from '../../../apis/snode_api/SnodeRequestTypes';
import { DeleteGroupHashesFactory } from '../../../apis/snode_api/factories/DeleteGroupHashesRequestFactory'; import { DeleteGroupHashesFactory } from '../../../apis/snode_api/factories/DeleteGroupHashesRequestFactory';
import { StoreGroupRequestFactory } from '../../../apis/snode_api/factories/StoreGroupRequestFactory'; import { StoreGroupRequestFactory } from '../../../apis/snode_api/factories/StoreGroupRequestFactory';
@ -138,6 +140,13 @@ async function pushChangesToGroupSwarmIfNeeded({
deleteAllMessagesSubRequest, deleteAllMessagesSubRequest,
]); ]);
const extraRequestWithExpectedResults = extraRequests.filter(
m =>
m instanceof SubaccountRevokeSubRequest ||
m instanceof SubaccountUnrevokeSubRequest ||
m instanceof DeleteAllFromGroupMsgNodeSubRequest
);
const result = await MessageSender.sendEncryptedDataToSnode({ const result = await MessageSender.sendEncryptedDataToSnode({
// Note: this is on purpose that supplementalKeysSubRequest is before pendingConfigRequests // Note: this is on purpose that supplementalKeysSubRequest is before pendingConfigRequests
// as this is to avoid a race condition where a device is polling right // as this is to avoid a race condition where a device is polling right
@ -155,11 +164,8 @@ async function pushChangesToGroupSwarmIfNeeded({
const expectedReplyLength = const expectedReplyLength =
pendingConfigRequests.length + // each of those are sent as a subrequest pendingConfigRequests.length + // each of those are sent as a subrequest
(supplementalKeysSubRequest ? 1 : 0) + // we are sending all the supplemental keys as a single subrequest (supplementalKeysSubRequest ? 1 : 0) + // we are sending all the supplemental keys as a single subrequest
(deleteHashesSubRequest ? 1 : 0) + // we are sending all hashes changes as a single subrequest (extraStoreRequests ? 1 : 0) + // each of those are sent as a subrequest
(revokeSubRequest ? 1 : 0) + // we are sending all revoke updates as a single subrequest extraRequestWithExpectedResults.length; // each of those are sent as a subrequest, but they don't all return something...
(unrevokeSubRequest ? 1 : 0) + // we are sending all revoke updates as a single subrequest
(deleteAllMessagesSubRequest ? 1 : 0) + // a delete_all sub request is a single subrequest
(extraStoreRequests ? 1 : 0); // each of those are sent as a subrequest
// we do a sequence call here. If we do not have the right expected number of results, consider it a failure // we do a sequence call here. If we do not have the right expected number of results, consider it a failure
if (!isArray(result) || result.length !== expectedReplyLength) { if (!isArray(result) || result.length !== expectedReplyLength) {

@ -351,6 +351,8 @@ async function saveDumpsToDb(pubkey: PubkeyType | GroupPubkeyType) {
const metaNeedsDump = await MetaGroupWrapperActions.needsDump(pubkey); const metaNeedsDump = await MetaGroupWrapperActions.needsDump(pubkey);
// save the concatenated dumps as a single entry in the DB if any of the dumps had a need for dump // save the concatenated dumps as a single entry in the DB if any of the dumps had a need for dump
if (metaNeedsDump) { if (metaNeedsDump) {
window.log.debug(`About to make and save dumps for metagroup ${ed25519Str(pubkey)}`);
const dump = await MetaGroupWrapperActions.metaDump(pubkey); const dump = await MetaGroupWrapperActions.metaDump(pubkey);
await ConfigDumpData.saveConfigDump({ await ConfigDumpData.saveConfigDump({
data: dump, data: dump,
@ -358,7 +360,7 @@ async function saveDumpsToDb(pubkey: PubkeyType | GroupPubkeyType) {
variant: `MetaGroupConfig-${pubkey}`, variant: `MetaGroupConfig-${pubkey}`,
}); });
window.log.debug(`Saved dumps for metagroup ${ed25519Str(pubkey)}`); window.log.info(`Saved dumps for metagroup ${ed25519Str(pubkey)}`);
} else { } else {
window.log.debug(`No need to update local dumps for metagroup ${ed25519Str(pubkey)}`); window.log.debug(`No need to update local dumps for metagroup ${ed25519Str(pubkey)}`);
} }

Loading…
Cancel
Save