Merge pull request #20 from Bilb/fix-extra-issues-groups

Fix extra issues groups
pull/3281/head
Audric Ackermann 3 months ago committed by GitHub
commit 4661c89774
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -92,7 +92,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": "https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.4.10/libsession_util_nodejs-v0.4.10.tar.gz", "libsession_util_nodejs": "https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.4.11/libsession_util_nodejs-v0.4.11.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",

@ -41,7 +41,7 @@ window.sessionFeatureFlags = {
useOnionRequests: true, useOnionRequests: true,
useTestNet: isTestNet() || isTestIntegration(), useTestNet: isTestNet() || isTestIntegration(),
useClosedGroupV2: true, // TODO DO NOT MERGE Remove after QA useClosedGroupV2: true, // TODO DO NOT MERGE Remove after QA
useClosedGroupV2QAButtons: false, // TODO DO NOT MERGE Remove after QA useClosedGroupV2QAButtons: true, // TODO DO NOT MERGE Remove after QA
replaceLocalizedStringsWithKeys: false, replaceLocalizedStringsWithKeys: false,
debug: { debug: {
debugLogging: !_.isEmpty(process.env.SESSION_DEBUG), debugLogging: !_.isEmpty(process.env.SESSION_DEBUG),

@ -174,7 +174,7 @@ function localisedStatusFromMemberStatus(memberStatus: MemberStateGroupV2) {
case 'REMOVED_UNKNOWN': // fallback, hopefully won't happen in production case 'REMOVED_UNKNOWN': // fallback, hopefully won't happen in production
case 'REMOVED_MEMBER': // we want pending removal members at the end of the "invite" states case 'REMOVED_MEMBER': // we want pending removal members at the end of the "invite" states
case 'REMOVED_MEMBER_AND_MESSAGES': case 'REMOVED_MEMBER_AND_MESSAGES':
return null; // no text for those 3 pending removal states return window.i18n('groupPendingRemoval'); // no text for those 3 pending removal states
case 'PROMOTION_FAILED': case 'PROMOTION_FAILED':
return window.i18n('adminPromotionFailed'); return window.i18n('adminPromotionFailed');
case 'PROMOTION_NOT_SENT': case 'PROMOTION_NOT_SENT':
@ -259,14 +259,12 @@ const ResendButton = ({ groupPk, pubkey }: { pubkey: PubkeyType; groupPk: GroupP
window.log.warn('tried to resend invite but we do not have correct details'); window.log.warn('tried to resend invite but we do not have correct details');
return; return;
} }
await MetaGroupWrapperActions.memberSetInviteNotSent(groupPk, pubkey);
// if we tried to invite that member as admin right away, let's retry it as such. // if we tried to invite that member as admin right away, let's retry it as such.
const inviteAsAdmin = member.nominatedAdmin;
await GroupInvite.addJob({ await GroupInvite.addJob({
groupPk, groupPk,
member: pubkey, member: pubkey,
inviteAsAdmin, inviteAsAdmin: member.nominatedAdmin,
forceUnrevoke: true, forceUnrevoke: true,
}); });
}} }}

@ -1,7 +1,7 @@
import { compact, flatten, isEqual } from 'lodash'; import { compact, flatten, isEqual } from 'lodash';
import { SessionDataTestId, useEffect, useState } from 'react'; import { SessionDataTestId, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch } from 'react-redux';
import useInterval from 'react-use/lib/useInterval'; import useInterval from 'react-use/lib/useInterval';
import styled from 'styled-components'; import styled from 'styled-components';
import { Data } from '../../../../data/data'; import { Data } from '../../../../data/data';
@ -11,13 +11,14 @@ import {
useConversationUsername, useConversationUsername,
useDisappearingMessageSettingText, useDisappearingMessageSettingText,
useIsClosedGroup, useIsClosedGroup,
useIsGroupDestroyed,
useIsKickedFromGroup, useIsKickedFromGroup,
useIsPublic, useIsPublic,
useLastMessageIsLeaveError,
} from '../../../../hooks/useParamSelector'; } from '../../../../hooks/useParamSelector';
import { useIsRightPanelShowing } from '../../../../hooks/useUI'; import { useIsRightPanelShowing } from '../../../../hooks/useUI';
import { import {
showAddModeratorsByConvoId, showAddModeratorsByConvoId,
showDeleteGroupByConvoId,
showInviteContactByConvoId, showInviteContactByConvoId,
showLeaveGroupByConvoId, showLeaveGroupByConvoId,
showRemoveModeratorsByConvoId, showRemoveModeratorsByConvoId,
@ -57,7 +58,7 @@ import {
showDeleteGroupItem, showDeleteGroupItem,
showLeaveGroupItem, showLeaveGroupItem,
} from '../../../menu/items/LeaveAndDeleteGroup/guard'; } from '../../../menu/items/LeaveAndDeleteGroup/guard';
import { getIsMessageRequestOverlayShown } from '../../../../state/selectors/section'; import { useIsMessageRequestOverlayShown } from '../../../../state/selectors/section';
import { showLeaveCommunityItem } from '../../../menu/items/LeaveCommunity/guard'; import { showLeaveCommunityItem } from '../../../menu/items/LeaveCommunity/guard';
async function getMediaGalleryProps(conversationId: string): Promise<{ async function getMediaGalleryProps(conversationId: string): Promise<{
@ -229,16 +230,18 @@ const LeaveCommunityPanelButton = () => {
const DeleteGroupPanelButton = () => { const DeleteGroupPanelButton = () => {
const convoId = useSelectedConversationKey(); const convoId = useSelectedConversationKey();
const isGroup = useIsClosedGroup(convoId); const isGroup = useIsClosedGroup(convoId);
const isMessageRequestShown = useSelector(getIsMessageRequestOverlayShown); const isMessageRequestShown = useIsMessageRequestOverlayShown();
const isKickedFromGroup = useIsKickedFromGroup(convoId) || false; const isKickedFromGroup = useIsKickedFromGroup(convoId) || false;
const lastMessageIsLeaveError = useLastMessageIsLeaveError(convoId);
const selectedUsername = useConversationUsername(convoId) || convoId; const selectedUsername = useConversationUsername(convoId) || convoId;
const isPublic = useIsPublic(convoId);
const isGroupDestroyed = useIsGroupDestroyed(convoId);
const showItem = showDeleteGroupItem({ const showItem = showDeleteGroupItem({
isGroup, isGroup,
isKickedFromGroup, isKickedFromGroup,
isMessageRequestShown, isMessageRequestShown,
lastMessageIsLeaveError, isPublic,
isGroupDestroyed,
}); });
if (!showItem || !convoId) { if (!showItem || !convoId) {
@ -251,7 +254,7 @@ const DeleteGroupPanelButton = () => {
<PanelIconButton <PanelIconButton
text={window.i18n(token)} text={window.i18n(token)}
dataTestId="leave-group-button" dataTestId="leave-group-button"
onClick={() => void showLeaveGroupByConvoId(convoId, selectedUsername)} onClick={() => void showDeleteGroupByConvoId(convoId, selectedUsername)}
color={'var(--danger-color)'} color={'var(--danger-color)'}
iconType={'delete'} iconType={'delete'}
/> />
@ -262,15 +265,17 @@ const LeaveGroupPanelButton = () => {
const selectedConvoKey = useSelectedConversationKey(); const selectedConvoKey = useSelectedConversationKey();
const isGroup = useIsClosedGroup(selectedConvoKey); const isGroup = useIsClosedGroup(selectedConvoKey);
const username = useConversationUsername(selectedConvoKey) || selectedConvoKey; const username = useConversationUsername(selectedConvoKey) || selectedConvoKey;
const isMessageRequestShown = useSelector(getIsMessageRequestOverlayShown); const isMessageRequestShown = useIsMessageRequestOverlayShown();
const isKickedFromGroup = useIsKickedFromGroup(selectedConvoKey) || false; const isKickedFromGroup = useIsKickedFromGroup(selectedConvoKey) || false;
const lastMessageIsLeaveError = useLastMessageIsLeaveError(selectedConvoKey); const isPublic = useIsPublic(selectedConvoKey);
const isGroupDestroyed = useIsGroupDestroyed(selectedConvoKey);
const showItem = showLeaveGroupItem({ const showItem = showLeaveGroupItem({
isGroup, isGroup,
isKickedFromGroup, isKickedFromGroup,
isMessageRequestShown, isMessageRequestShown,
lastMessageIsLeaveError, isPublic,
isGroupDestroyed,
}); });
if (!selectedConvoKey || !showItem) { if (!selectedConvoKey || !showItem) {

@ -1,7 +1,6 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { useSelector } from 'react-redux';
import { useConvoIdFromContext } from '../../../contexts/ConvoIdContext'; import { useConvoIdFromContext } from '../../../contexts/ConvoIdContext';
import { import {
useHasUnread, useHasUnread,
@ -12,7 +11,7 @@ import {
} from '../../../hooks/useParamSelector'; } from '../../../hooks/useParamSelector';
import { LastMessageStatusType } from '../../../state/ducks/types'; import { LastMessageStatusType } from '../../../state/ducks/types';
import { useIsSearching } from '../../../state/selectors/search'; import { useIsSearching } from '../../../state/selectors/search';
import { getIsMessageRequestOverlayShown } from '../../../state/selectors/section'; import { useIsMessageRequestOverlayShown } from '../../../state/selectors/section';
import { assertUnreachable } from '../../../types/sqlSharedTypes'; import { assertUnreachable } from '../../../types/sqlSharedTypes';
import { TypingAnimation } from '../../conversation/TypingAnimation'; import { TypingAnimation } from '../../conversation/TypingAnimation';
import { MessageBody } from '../../conversation/message/message-content/MessageBody'; import { MessageBody } from '../../conversation/message/message-content/MessageBody';
@ -26,7 +25,7 @@ export const MessageItem = () => {
const hasUnread = useHasUnread(conversationId); const hasUnread = useHasUnread(conversationId);
const isConvoTyping = useIsTyping(conversationId); const isConvoTyping = useIsTyping(conversationId);
const isMessageRequest = useSelector(getIsMessageRequestOverlayShown); const isMessageRequest = useIsMessageRequestOverlayShown();
const isOutgoingRequest = useIsOutgoingRequest(conversationId); const isOutgoingRequest = useIsOutgoingRequest(conversationId);
const isSearching = useIsSearching(); const isSearching = useIsSearching();

@ -5,8 +5,8 @@ import { useConvoIdFromContext } from '../../contexts/ConvoIdContext';
import { useIsPinned, useIsPrivate, useIsPrivateAndFriend } from '../../hooks/useParamSelector'; import { useIsPinned, useIsPrivate, useIsPrivateAndFriend } from '../../hooks/useParamSelector';
import { ConvoHub } from '../../session/conversations'; import { ConvoHub } from '../../session/conversations';
import { import {
getIsMessageRequestOverlayShown,
getIsMessageSection, getIsMessageSection,
useIsMessageRequestOverlayShown,
} from '../../state/selectors/section'; } from '../../state/selectors/section';
import { useIsSearching } from '../../state/selectors/search'; import { useIsSearching } from '../../state/selectors/search';
import { SessionContextMenuContainer } from '../SessionContextMenuContainer'; import { SessionContextMenuContainer } from '../SessionContextMenuContainer';
@ -91,7 +91,7 @@ export const PinConversationMenuItem = (): JSX.Element | null => {
const isPrivateAndFriend = useIsPrivateAndFriend(conversationId); const isPrivateAndFriend = useIsPrivateAndFriend(conversationId);
const isPrivate = useIsPrivate(conversationId); const isPrivate = useIsPrivate(conversationId);
const isPinned = useIsPinned(conversationId); const isPinned = useIsPinned(conversationId);
const isMessageRequest = useSelector(getIsMessageRequestOverlayShown); const isMessageRequest = useIsMessageRequestOverlayShown();
if (isMessagesSection && !isMessageRequest && (!isPrivate || (isPrivate && isPrivateAndFriend))) { if (isMessagesSection && !isMessageRequest && (!isPrivate || (isPrivate && isPrivateAndFriend))) {
const conversation = ConvoHub.use().get(conversationId); const conversation = ConvoHub.use().get(conversationId);

@ -49,8 +49,8 @@ import {
} from '../../state/ducks/modalDialog'; } from '../../state/ducks/modalDialog';
import { useConversationIdOrigin } from '../../state/selectors/conversations'; import { useConversationIdOrigin } from '../../state/selectors/conversations';
import { import {
getIsMessageRequestOverlayShown,
getIsMessageSection, getIsMessageSection,
useIsMessageRequestOverlayShown,
} from '../../state/selectors/section'; } from '../../state/selectors/section';
import { useSelectedConversationKey } from '../../state/selectors/selectedConversation'; import { useSelectedConversationKey } from '../../state/selectors/selectedConversation';
import { SessionButtonColor } from '../basic/SessionButton'; import { SessionButtonColor } from '../basic/SessionButton';
@ -84,7 +84,7 @@ export const MarkConversationUnreadMenuItem = (): JSX.Element | null => {
const isMessagesSection = useSelector(getIsMessageSection); const isMessagesSection = useSelector(getIsMessageSection);
const isPrivate = useIsPrivate(conversationId); const isPrivate = useIsPrivate(conversationId);
const isPrivateAndFriend = useIsPrivateAndFriend(conversationId); const isPrivateAndFriend = useIsPrivateAndFriend(conversationId);
const isMessageRequestShown = useSelector(getIsMessageRequestOverlayShown); const isMessageRequestShown = useIsMessageRequestOverlayShown();
if ( if (
isMessagesSection && isMessagesSection &&
@ -358,7 +358,7 @@ export const ChangeNicknameMenuItem = () => {
*/ */
export const DeleteMessagesMenuItem = () => { export const DeleteMessagesMenuItem = () => {
const convoId = useConvoIdFromContext(); const convoId = useConvoIdFromContext();
const isMessageRequestShown = useSelector(getIsMessageRequestOverlayShown); const isMessageRequestShown = useIsMessageRequestOverlayShown();
if (!convoId || isMessageRequestShown) { if (!convoId || isMessageRequestShown) {
return null; return null;
@ -495,7 +495,7 @@ export const NotificationForConvoMenuItem = (): JSX.Element | null => {
const isFriend = useIsPrivateAndFriend(convoId); const isFriend = useIsPrivateAndFriend(convoId);
const isPrivate = useIsPrivate(convoId); const isPrivate = useIsPrivate(convoId);
const isMessageRequestShown = useSelector(getIsMessageRequestOverlayShown); const isMessageRequestShown = useIsMessageRequestOverlayShown();
if ( if (
!convoId || !convoId ||

@ -1,14 +1,14 @@
import { useSelector } from 'react-redux';
import { useConvoIdFromContext } from '../../../../contexts/ConvoIdContext'; import { useConvoIdFromContext } from '../../../../contexts/ConvoIdContext';
import { import {
useConversationUsername, useConversationUsername,
useIsKickedFromGroup, useIsKickedFromGroup,
useIsClosedGroup, useIsClosedGroup,
useLastMessageIsLeaveError, useIsPublic,
useIsGroupDestroyed,
} from '../../../../hooks/useParamSelector'; } from '../../../../hooks/useParamSelector';
import { showLeaveGroupByConvoId } from '../../../../interactions/conversationInteractions'; import { showDeleteGroupByConvoId } from '../../../../interactions/conversationInteractions';
import { PubKey } from '../../../../session/types'; import { PubKey } from '../../../../session/types';
import { getIsMessageRequestOverlayShown } from '../../../../state/selectors/section'; import { useIsMessageRequestOverlayShown } from '../../../../state/selectors/section';
import { ItemWithDataTestId } from '../MenuItemWithDataTestId'; import { ItemWithDataTestId } from '../MenuItemWithDataTestId';
import { showDeleteGroupItem } from './guard'; import { showDeleteGroupItem } from './guard';
import { Localizer } from '../../../basic/Localizer'; import { Localizer } from '../../../basic/Localizer';
@ -17,15 +17,17 @@ export const DeleteGroupMenuItem = () => {
const convoId = useConvoIdFromContext(); const convoId = useConvoIdFromContext();
const username = useConversationUsername(convoId) || convoId; const username = useConversationUsername(convoId) || convoId;
const isGroup = useIsClosedGroup(convoId); const isGroup = useIsClosedGroup(convoId);
const isMessageRequestShown = useSelector(getIsMessageRequestOverlayShown); const isMessageRequestShown = useIsMessageRequestOverlayShown();
const isKickedFromGroup = useIsKickedFromGroup(convoId) || false; const isKickedFromGroup = useIsKickedFromGroup(convoId) || false;
const lastMessageIsLeaveError = useLastMessageIsLeaveError(convoId); const isPublic = useIsPublic(convoId);
const isGroupDestroyed = useIsGroupDestroyed(convoId);
const showLeave = showDeleteGroupItem({ const showLeave = showDeleteGroupItem({
isGroup, isGroup,
isKickedFromGroup, isKickedFromGroup,
isMessageRequestShown, isMessageRequestShown,
lastMessageIsLeaveError, isPublic,
isGroupDestroyed,
}); });
if (!showLeave) { if (!showLeave) {
@ -37,7 +39,7 @@ export const DeleteGroupMenuItem = () => {
return ( return (
<ItemWithDataTestId <ItemWithDataTestId
onClick={() => { onClick={() => {
void showLeaveGroupByConvoId(convoId, username); void showDeleteGroupByConvoId(convoId, username);
}} }}
> >
<Localizer token={token} /> <Localizer token={token} />

@ -1,13 +1,13 @@
import { useSelector } from 'react-redux';
import { useConvoIdFromContext } from '../../../../contexts/ConvoIdContext'; import { useConvoIdFromContext } from '../../../../contexts/ConvoIdContext';
import { import {
useConversationUsername, useConversationUsername,
useIsKickedFromGroup, useIsKickedFromGroup,
useIsClosedGroup, useIsClosedGroup,
useLastMessageIsLeaveError, useIsPublic,
useIsGroupDestroyed,
} from '../../../../hooks/useParamSelector'; } from '../../../../hooks/useParamSelector';
import { showLeaveGroupByConvoId } from '../../../../interactions/conversationInteractions'; import { showLeaveGroupByConvoId } from '../../../../interactions/conversationInteractions';
import { getIsMessageRequestOverlayShown } from '../../../../state/selectors/section'; import { useIsMessageRequestOverlayShown } from '../../../../state/selectors/section';
import { ItemWithDataTestId } from '../MenuItemWithDataTestId'; import { ItemWithDataTestId } from '../MenuItemWithDataTestId';
import { showLeaveGroupItem } from './guard'; import { showLeaveGroupItem } from './guard';
import { Localizer } from '../../../basic/Localizer'; import { Localizer } from '../../../basic/Localizer';
@ -15,16 +15,18 @@ import { Localizer } from '../../../basic/Localizer';
export const LeaveGroupMenuItem = () => { export const LeaveGroupMenuItem = () => {
const convoId = useConvoIdFromContext(); const convoId = useConvoIdFromContext();
const isGroup = useIsClosedGroup(convoId); const isGroup = useIsClosedGroup(convoId);
const isPublic = useIsPublic(convoId);
const username = useConversationUsername(convoId) || convoId; const username = useConversationUsername(convoId) || convoId;
const isMessageRequestShown = useSelector(getIsMessageRequestOverlayShown); const isMessageRequestShown = useIsMessageRequestOverlayShown();
const isKickedFromGroup = useIsKickedFromGroup(convoId) || false; const isKickedFromGroup = useIsKickedFromGroup(convoId) || false;
const lastMessageIsLeaveError = useLastMessageIsLeaveError(convoId); const isGroupDestroyed = useIsGroupDestroyed(convoId);
const showLeave = showLeaveGroupItem({ const showLeave = showLeaveGroupItem({
isGroup, isGroup,
isMessageRequestShown, isMessageRequestShown,
isKickedFromGroup, isKickedFromGroup,
lastMessageIsLeaveError, isPublic,
isGroupDestroyed,
}); });
if (!showLeave) { if (!showLeave) {

@ -1,42 +1,53 @@
function sharedEnabled({ function sharedEnabled({
isGroup, isGroup,
isPublic,
isMessageRequestShown, isMessageRequestShown,
}: Pick<Parameters<typeof showLeaveGroupItem>[0], 'isGroup' | 'isMessageRequestShown'>) { }: Pick<
return isGroup && !isMessageRequestShown; Parameters<typeof showLeaveGroupItem>[0],
'isGroup' | 'isMessageRequestShown' | 'isPublic'
>) {
return isGroup && !isMessageRequestShown && !isPublic;
} }
/**
* We can try leave a group if
* - we are an admin of the group (that group would be marked as destroyed on delete)
* and
* - we are a **not kicked** member (if we are kicked without knowing about it and try to leave, we will silently remove the group)
*
* Note: Those actions are hidden if the group is a group request (as we have other buttons to accept/decline a group request).
*
* Note: If we fail to leave the group but that error is retryable, we will keep the group displaying the "leave" option.
*/
export function showLeaveGroupItem({ export function showLeaveGroupItem({
isGroup, isGroup,
isPublic,
isKickedFromGroup, isKickedFromGroup,
isMessageRequestShown, isMessageRequestShown,
lastMessageIsLeaveError, isGroupDestroyed,
}: { }: {
isGroup: boolean; isGroup: boolean;
isPublic: boolean;
isMessageRequestShown: boolean; isMessageRequestShown: boolean;
lastMessageIsLeaveError: boolean;
isKickedFromGroup: boolean; isKickedFromGroup: boolean;
isGroupDestroyed: boolean;
}) { }) {
// we can't try to leave the group if we were kicked from it, or if we've already tried to (lastMessageIsLeaveError is true)
return ( return (
sharedEnabled({ isGroup, isMessageRequestShown }) && sharedEnabled({ isGroup, isMessageRequestShown, isPublic }) &&
!isKickedFromGroup && !isKickedFromGroup &&
!lastMessageIsLeaveError !isGroupDestroyed
); );
} }
export function showDeleteGroupItem({ /**
isGroup, * We can try to delete a group only if the `showLeaveGroupItem` returns false.
isKickedFromGroup, * Note: those actions are hidden if the group is a group request (as we have other buttons to accept/decline a group request)
isMessageRequestShown, */
lastMessageIsLeaveError, export function showDeleteGroupItem(args: {
}: {
isGroup: boolean; isGroup: boolean;
isPublic: boolean;
isMessageRequestShown: boolean; isMessageRequestShown: boolean;
lastMessageIsLeaveError: boolean;
isKickedFromGroup: boolean; isKickedFromGroup: boolean;
isGroupDestroyed: boolean;
}) { }) {
return ( return sharedEnabled(args) && !showLeaveGroupItem(args);
sharedEnabled({ isGroup, isMessageRequestShown }) &&
(isKickedFromGroup || lastMessageIsLeaveError)
);
} }

@ -263,7 +263,7 @@ async function removeMessage(id: string): Promise<void> {
// it needs to delete all associated on-disk files along with the database delete. // it needs to delete all associated on-disk files along with the database delete.
if (message) { if (message) {
await channels.removeMessage(id); await channels.removeMessage(id);
await message.cleanup(); await message.cleanup(true);
} }
} }
@ -554,7 +554,7 @@ async function removeAllMessagesInConversation(conversationId: string): Promise<
for (let index = 0; index < messages.length; index++) { for (let index = 0; index < messages.length; index++) {
const message = messages.at(index); const message = messages.at(index);
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await message.cleanup(); await message.cleanup(false); // not triggering UI updates, as we remove them from the store just below
} }
window.log.info( window.log.info(
`removeAllMessagesInConversation messages.cleanup() ${conversationId} took ${ `removeAllMessagesInConversation messages.cleanup() ${conversationId} took ${

@ -212,7 +212,7 @@ export function useIsKickedFromGroup(convoId?: string) {
export function useIsGroupDestroyed(convoId?: string) { export function useIsGroupDestroyed(convoId?: string) {
const libIsDestroyed = useLibGroupDestroyed(convoId); const libIsDestroyed = useLibGroupDestroyed(convoId);
if (convoId && PubKey.is03Pubkey(convoId)) { if (convoId && PubKey.is03Pubkey(convoId)) {
return libIsDestroyed; return !!libIsDestroyed;
} }
return false; return false;
} }

@ -431,21 +431,6 @@ async function leaveGroupOrCommunityByConvoId({
} }
} }
/**
* Returns true if we the convo is a 03 group and if we can try to send a leave message.
*/
async function hasLeavingDetails(convoId: string) {
if (!PubKey.is03Pubkey(convoId)) {
return true;
}
const group = await UserGroupsWrapperActions.getGroup(convoId);
// we need the authData or the secretKey to be able to attempt to leave,
// otherwise we won't be able to even try
return group && (!isEmpty(group.authData) || !isEmpty(group.secretKey));
}
export async function showLeaveGroupByConvoId(conversationId: string, name: string | undefined) { export async function showLeaveGroupByConvoId(conversationId: string, name: string | undefined) {
const conversation = ConvoHub.use().get(conversationId); const conversation = ConvoHub.use().get(conversationId);
@ -462,20 +447,6 @@ export async function showLeaveGroupByConvoId(conversationId: string, name: stri
(PubKey.is05Pubkey(conversationId) || PubKey.is03Pubkey(conversationId)) && (PubKey.is05Pubkey(conversationId) || PubKey.is03Pubkey(conversationId)) &&
isAdmin && isAdmin &&
admins.length === 1; admins.length === 1;
const lastMessageInteractionType = conversation.get('lastMessageInteractionType');
const lastMessageInteractionStatus = conversation.get('lastMessageInteractionStatus');
const canTryToLeave = await hasLeavingDetails(conversationId);
if (
!isPublic &&
((lastMessageInteractionType === ConversationInteractionType.Leave &&
lastMessageInteractionStatus === ConversationInteractionStatus.Error) ||
!canTryToLeave) // if we don't have any key to send our leave message, no need to try
) {
await leaveGroupOrCommunityByConvoId({ conversationId, isPublic, sendLeaveMessage: false });
return;
}
// if this is a community, or we legacy group are not admin, we can just show a confirmation dialog // if this is a community, or we legacy group are not admin, we can just show a confirmation dialog
@ -525,6 +496,46 @@ export async function showLeaveGroupByConvoId(conversationId: string, name: stri
} }
} }
/**
* Can be used to show a dialog asking confirmation about deleting a group.
* Communities are explicitly forbidden.
* This function won't attempt to send a leave message. Use `showLeaveGroupByConvoId` for that purpose
*/
export async function showDeleteGroupByConvoId(conversationId: string, name: string | undefined) {
const conversation = ConvoHub.use().get(conversationId);
const isPublic = conversation.isPublic();
if (!conversation.isGroup() || isPublic) {
throw new Error('showDeleteGroupByConvoId() called with a non group convo.');
}
const onClickClose = () => {
window?.inboxStore?.dispatch(updateConfirmModal(null));
};
const onClickOk = async () => {
await leaveGroupOrCommunityByConvoId({
conversationId,
isPublic, // we check for isPublic above, and throw if it's true
sendLeaveMessage: false,
onClickClose,
});
};
window?.inboxStore?.dispatch(
updateConfirmModal({
title: window.i18n('groupDelete'),
i18nMessage: { token: 'groupDeleteDescriptionMember', args: { group_name: name ?? '' } },
onClickOk,
okText: window.i18n('delete'),
okTheme: SessionButtonColor.Danger,
onClickClose,
conversationId,
})
);
}
export function showInviteContactByConvoId(conversationId: string) { export function showInviteContactByConvoId(conversationId: string) {
window.inboxStore?.dispatch(updateInviteContactModal({ conversationId })); window.inboxStore?.dispatch(updateInviteContactModal({ conversationId }));
} }
@ -1027,6 +1038,7 @@ export async function promoteUsersInGroup({
method: 'batch', method: 'batch',
sortedSubRequests: storeRequests, sortedSubRequests: storeRequests,
abortSignal: controller.signal, abortSignal: controller.signal,
allow401s: false,
}), }),
2 * DURATION.MINUTES, 2 * DURATION.MINUTES,
controller controller

@ -1148,6 +1148,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
await GroupSync.pushChangesToGroupSwarmIfNeeded({ await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk: this.id, groupPk: this.id,
extraStoreRequests, extraStoreRequests,
allow401s: false,
}); });
await GroupSync.queueNewJobIfNeeded(this.id); await GroupSync.queueNewJobIfNeeded(this.id);

@ -442,10 +442,10 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
return ''; return '';
} }
public async cleanup() { public async cleanup(triggerUIUpdate: boolean) {
const changed = await deleteExternalMessageFiles(this.attributes); const changed = await deleteExternalMessageFiles(this.attributes);
if (changed) { if (changed) {
await this.commit(); await this.commit(triggerUIUpdate);
} }
} }

@ -2,7 +2,6 @@ import { isArray } from 'lodash';
import { AbortController } from 'abort-controller'; import { AbortController } from 'abort-controller';
import { MessageSender } from '../../sending'; import { MessageSender } from '../../sending';
import { Snode } from '../../../data/types';
import { SnodeResponseError } from '../../utils/errors'; import { SnodeResponseError } from '../../utils/errors';
import { processOnionRequestErrorAtDestination, SnodeResponse } from './onions'; import { processOnionRequestErrorAtDestination, SnodeResponse } from './onions';
import { SessionRpc } from './sessionRpc'; import { SessionRpc } from './sessionRpc';
@ -15,15 +14,12 @@ import {
} from './SnodeRequestTypes'; } from './SnodeRequestTypes';
import { NotEmptyArrayOfBatchResults } from './BatchResultEntry'; import { NotEmptyArrayOfBatchResults } from './BatchResultEntry';
import { MergedAbortSignal, WithTimeoutMs } from './requestWith'; import { MergedAbortSignal, WithTimeoutMs } from './requestWith';
import { WithAllow401s, WithAssociatedWith, WithTargetNode } from '../../types/with';
function logSubRequests(requests: Array<BuiltSnodeSubRequests>) { function logSubRequests(requests: Array<BuiltSnodeSubRequests>) {
return `[${requests.map(builtRequestToLoggingId).join(', ')}]`; return `[${requests.map(builtRequestToLoggingId).join(', ')}]`;
} }
type WithTargetNode = { targetNode: Snode };
type WithAssociatedWith = { associatedWith: string | null };
type WithAllow401s = { allow401s: boolean };
/** /**
* This is the equivalent to the batch send on sogs. The target node runs each sub request and returns a list of all the sub status and bodies. * This is the equivalent to the batch send on sogs. The target node runs each sub request and returns a list of all the sub status and bodies.
* If the global status code is not 200, an exception is thrown. * If the global status code is not 200, an exception is thrown.

@ -21,6 +21,13 @@ import { fileServerHost } from '../file_server_api/FileServerApi';
import { hrefPnServerProd } from '../push_notification_api/PnServer'; import { hrefPnServerProd } from '../push_notification_api/PnServer';
import { ERROR_CODE_NO_CONNECT } from './SNodeAPI'; import { ERROR_CODE_NO_CONNECT } from './SNodeAPI';
import { MergedAbortSignal, WithAbortSignal, WithTimeoutMs } from './requestWith'; import { MergedAbortSignal, WithAbortSignal, WithTimeoutMs } from './requestWith';
import {
WithAllow401s,
WithAssociatedWith,
WithDestinationEd25519,
WithGuardNode,
WithSymmetricKey,
} from '../../types/with';
// hold the ed25519 key of a snode against the time it fails. Used to remove a snode only after a few failures (snodeFailureThreshold failures) // hold the ed25519 key of a snode against the time it fails. Used to remove a snode only after a few failures (snodeFailureThreshold failures)
let snodeFailureCount: Record<string, number> = {}; let snodeFailureCount: Record<string, number> = {};
@ -310,13 +317,11 @@ export async function processOnionRequestErrorAtDestination({
destinationSnodeEd25519, destinationSnodeEd25519,
associatedWith, associatedWith,
allow401s, allow401s,
}: { }: WithAllow401s &
statusCode: number; Partial<WithDestinationEd25519 & WithAssociatedWith> & {
body: string; statusCode: number;
destinationSnodeEd25519?: string; body: string;
associatedWith?: string; }) {
allow401s: boolean;
}) {
if (statusCode === 200) { if (statusCode === 200) {
return; return;
} }
@ -328,13 +333,13 @@ export async function processOnionRequestErrorAtDestination({
process401Error(statusCode); process401Error(statusCode);
} }
processOxenServerError(statusCode, body); processOxenServerError(statusCode, body);
await process421Error(statusCode, body, associatedWith, destinationSnodeEd25519); await process421Error(statusCode, body, associatedWith || undefined, destinationSnodeEd25519);
if (destinationSnodeEd25519) { if (destinationSnodeEd25519) {
await processAnyOtherErrorAtDestination( await processAnyOtherErrorAtDestination(
statusCode, statusCode,
body, body,
destinationSnodeEd25519, destinationSnodeEd25519,
associatedWith associatedWith || undefined
); );
} }
} }
@ -342,9 +347,8 @@ export async function processOnionRequestErrorAtDestination({
async function handleNodeNotFound({ async function handleNodeNotFound({
ed25519NotFound, ed25519NotFound,
associatedWith, associatedWith,
}: { }: Partial<WithAssociatedWith> & {
ed25519NotFound: string; ed25519NotFound: string;
associatedWith?: string;
}) { }) {
const shortNodeNotFound = ed25519Str(ed25519NotFound); const shortNodeNotFound = ed25519Str(ed25519NotFound);
window?.log?.warn('Handling NODE NOT FOUND with: ', shortNodeNotFound); window?.log?.warn('Handling NODE NOT FOUND with: ', shortNodeNotFound);
@ -526,14 +530,11 @@ async function processOnionResponse({
associatedWith, associatedWith,
destinationSnodeEd25519, destinationSnodeEd25519,
allow401s, allow401s,
}: Partial<WithAbortSignal> & { }: Partial<WithAbortSignal & WithDestinationEd25519 & WithAssociatedWith & WithSymmetricKey> &
response?: { text: () => Promise<string>; status: number }; WithAllow401s &
symmetricKey?: ArrayBuffer; WithGuardNode & {
guardNode: Snode; response?: { text: () => Promise<string>; status: number };
destinationSnodeEd25519?: string; }): Promise<SnodeResponse> {
associatedWith?: string;
allow401s: boolean;
}): Promise<SnodeResponse> {
let ciphertext = ''; let ciphertext = '';
processAbortedRequest(abortSignal); processAbortedRequest(abortSignal);
@ -549,7 +550,7 @@ async function processOnionResponse({
ciphertext, ciphertext,
guardNode.pubkey_ed25519, guardNode.pubkey_ed25519,
destinationSnodeEd25519, destinationSnodeEd25519,
associatedWith associatedWith || undefined
); );
if (!ciphertext) { if (!ciphertext) {
@ -598,7 +599,7 @@ async function processOnionResponse({
} }
return value; return value;
}) as Record<string, any>; }) as Record<string, any>;
// TODO: type those status
const status = jsonRes.status_code || jsonRes.status; const status = jsonRes.status_code || jsonRes.status;
await processOnionRequestErrorAtDestination({ await processOnionRequestErrorAtDestination({
@ -642,13 +643,10 @@ async function processOnionResponseV4({
guardNode, guardNode,
destinationSnodeEd25519, destinationSnodeEd25519,
associatedWith, associatedWith,
}: Partial<WithAbortSignal> & { }: Partial<WithAbortSignal & WithDestinationEd25519 & WithAssociatedWith & WithSymmetricKey> &
response?: Response; WithGuardNode & {
symmetricKey?: ArrayBuffer; response?: Response;
guardNode: Snode; }): Promise<SnodeResponseV4 | undefined> {
destinationSnodeEd25519?: string;
associatedWith?: string;
}): Promise<SnodeResponseV4 | undefined> {
processAbortedRequest(abortSignal); processAbortedRequest(abortSignal);
const validSymmetricKey = await processNoSymmetricKeyError(guardNode, symmetricKey); const validSymmetricKey = await processNoSymmetricKeyError(guardNode, symmetricKey);
@ -669,7 +667,7 @@ async function processOnionResponseV4({
cipherText, cipherText,
guardNode.pubkey_ed25519, guardNode.pubkey_ed25519,
destinationSnodeEd25519, destinationSnodeEd25519,
associatedWith associatedWith || undefined
); );
const plaintextBuffer = await callUtilsWorker( const plaintextBuffer = await callUtilsWorker(
@ -705,9 +703,8 @@ export type FinalRelayOptions = {
port?: number; // default to 443 port?: number; // default to 443
}; };
export type DestinationContext = { export type DestinationContext = WithSymmetricKey & {
ciphertext: Uint8Array; ciphertext: Uint8Array;
symmetricKey: ArrayBuffer;
ephemeralKey: ArrayBuffer; ephemeralKey: ArrayBuffer;
}; };
@ -721,10 +718,8 @@ async function handle421InvalidSwarm({
body, body,
destinationSnodeEd25519, destinationSnodeEd25519,
associatedWith, associatedWith,
}: { }: Partial<WithDestinationEd25519 & WithAssociatedWith> & {
body: string; body: string;
destinationSnodeEd25519?: string;
associatedWith?: string;
}) { }) {
if (!destinationSnodeEd25519 || !associatedWith) { if (!destinationSnodeEd25519 || !associatedWith) {
// The snode isn't associated with the given public key anymore // The snode isn't associated with the given public key anymore
@ -784,9 +779,8 @@ async function handle421InvalidSwarm({
async function incrementBadSnodeCountOrDrop({ async function incrementBadSnodeCountOrDrop({
snodeEd25519, snodeEd25519,
associatedWith, associatedWith,
}: { }: Partial<WithAssociatedWith> & {
snodeEd25519: string; snodeEd25519: string;
associatedWith?: string;
}) { }) {
const oldFailureCount = snodeFailureCount[snodeEd25519] || 0; const oldFailureCount = snodeFailureCount[snodeEd25519] || 0;
const newFailureCount = oldFailureCount + 1; const newFailureCount = oldFailureCount + 1;
@ -828,15 +822,15 @@ async function sendOnionRequestHandlingSnodeEjectNoRetries({
allow401s, allow401s,
timeoutMs, timeoutMs,
}: WithAbortSignal & }: WithAbortSignal &
WithTimeoutMs & { WithTimeoutMs &
WithAllow401s &
Partial<WithAssociatedWith> & {
nodePath: Array<Snode>; nodePath: Array<Snode>;
destSnodeX25519: string; destSnodeX25519: string;
finalDestOptions: FinalDestOptions; finalDestOptions: FinalDestOptions;
finalRelayOptions?: FinalRelayOptions; finalRelayOptions?: FinalRelayOptions;
associatedWith?: string;
useV4: boolean; useV4: boolean;
throwErrors: boolean; throwErrors: boolean;
allow401s: boolean;
}): Promise<SnodeResponse | SnodeResponseV4 | undefined> { }): Promise<SnodeResponse | SnodeResponseV4 | undefined> {
// this sendOnionRequestNoRetries() call has to be the only one like this. // this sendOnionRequestNoRetries() call has to be the only one like this.
// If you need to call it, call it through sendOnionRequestHandlingSnodeEjectNoRetries because this is the one handling path rebuilding and known errors // If you need to call it, call it through sendOnionRequestHandlingSnodeEjectNoRetries because this is the one handling path rebuilding and known errors
@ -1118,13 +1112,13 @@ async function sendOnionRequestSnodeDestNoRetries({
timeoutMs, timeoutMs,
associatedWith, associatedWith,
}: WithTimeoutMs & }: WithTimeoutMs &
WithAbortSignal & { WithAbortSignal &
WithAllow401s &
Partial<WithAssociatedWith> & {
onionPath: Array<Snode>; onionPath: Array<Snode>;
targetNode: Snode; targetNode: Snode;
headers: Record<string, any>; headers: Record<string, any>;
plaintext: string | null; plaintext: string | null;
allow401s: boolean;
associatedWith?: string;
}) { }) {
return Onions.sendOnionRequestHandlingSnodeEjectNoRetries({ return Onions.sendOnionRequestHandlingSnodeEjectNoRetries({
nodePath: onionPath, nodePath: onionPath,
@ -1155,12 +1149,12 @@ async function lokiOnionFetchNoRetries({
abortSignal, abortSignal,
timeoutMs, timeoutMs,
}: WithTimeoutMs & }: WithTimeoutMs &
WithAbortSignal & { WithAbortSignal &
WithAllow401s &
Partial<WithAssociatedWith> & {
targetNode: Snode; targetNode: Snode;
headers: Record<string, any>; headers: Record<string, any>;
body: string | null; body: string | null;
associatedWith?: string;
allow401s: boolean;
}): Promise<SnodeResponse | undefined> { }): Promise<SnodeResponse | undefined> {
try { try {
// Get a path excluding `targetNode`: // Get a path excluding `targetNode`:

@ -10,6 +10,7 @@ import { HTTPError, NotFoundError } from '../../utils/errors';
import { APPLICATION_JSON } from '../../../types/MIME'; import { APPLICATION_JSON } from '../../../types/MIME';
import { ERROR_421_HANDLED_RETRY_REQUEST, Onions, snodeHttpsAgent, SnodeResponse } from './onions'; import { ERROR_421_HANDLED_RETRY_REQUEST, Onions, snodeHttpsAgent, SnodeResponse } from './onions';
import { WithAbortSignal, WithTimeoutMs } from './requestWith'; import { WithAbortSignal, WithTimeoutMs } from './requestWith';
import { WithAllow401s } from '../../types/with';
export interface LokiFetchOptions { export interface LokiFetchOptions {
method: 'GET' | 'POST'; method: 'GET' | 'POST';
@ -32,12 +33,12 @@ async function doRequestNoRetries({
allow401s, allow401s,
abortSignal, abortSignal,
}: WithTimeoutMs & }: WithTimeoutMs &
WithAbortSignal & { WithAbortSignal &
WithAllow401s & {
url: string; url: string;
options: LokiFetchOptions; options: LokiFetchOptions;
targetNode?: Snode; targetNode?: Snode;
associatedWith: string | null; associatedWith: string | null;
allow401s: boolean;
}): Promise<undefined | SnodeResponse> { }): Promise<undefined | SnodeResponse> {
const method = options.method || 'GET'; const method = options.method || 'GET';
@ -126,12 +127,12 @@ async function snodeRpcNoRetries(
timeoutMs, timeoutMs,
abortSignal, abortSignal,
}: WithTimeoutMs & }: WithTimeoutMs &
WithAllow401s &
WithAbortSignal & { WithAbortSignal & {
method: string; method: string;
params: Record<string, any> | Array<Record<string, any>>; params: Record<string, any> | Array<Record<string, any>>;
targetNode: Snode; targetNode: Snode;
associatedWith: string | null; associatedWith: string | null;
allow401s: boolean;
} // the user pubkey this call is for. if the onion request fails, this is used to handle the error for this user swarm for instance } // the user pubkey this call is for. if the onion request fails, this is used to handle the error for this user swarm for instance
): Promise<undefined | SnodeResponse> { ): Promise<undefined | SnodeResponse> {
const url = `https://${targetNode.ip}:${targetNode.port}/storage_rpc/v1`; const url = `https://${targetNode.ip}:${targetNode.port}/storage_rpc/v1`;

@ -286,8 +286,8 @@ class ConvoController {
const groupInUserGroup = await UserGroupsWrapperActions.getGroup(groupPk); const groupInUserGroup = await UserGroupsWrapperActions.getGroup(groupPk);
// send the leave message before we delete everything for this group (including the key!) // send the leave message before we delete everything for this group (including the key!)
// Note: if we were kicked, we already lost the authData/secretKey for it, so no need to try to send our message.
if (sendLeaveMessage && !groupInUserGroup?.kicked) { if (sendLeaveMessage) {
const failedToSendLeaveMessage = await leaveClosedGroup(groupPk, fromSyncMessage); const failedToSendLeaveMessage = await leaveClosedGroup(groupPk, fromSyncMessage);
if (PubKey.is03Pubkey(groupPk) && failedToSendLeaveMessage) { if (PubKey.is03Pubkey(groupPk) && failedToSendLeaveMessage) {
// this is caught and is adding an interaction notification message // this is caught and is adding an interaction notification message
@ -369,6 +369,7 @@ class ConvoController {
groupPk, groupPk,
deleteAllMessagesSubRequest, deleteAllMessagesSubRequest,
extraStoreRequests: [], extraStoreRequests: [],
allow401s: false,
}); });
await LibSessionUtil.saveDumpsToDb(groupPk); await LibSessionUtil.saveDumpsToDb(groupPk);
@ -573,7 +574,7 @@ class ConvoController {
throw new Error(`ConvoHub.${deleteType} needs complete initial fetch`); throw new Error(`ConvoHub.${deleteType} needs complete initial fetch`);
} }
window.log.info(`${deleteType} with ${ed25519Str(convoId)}`); window.log.info(`deleteConvoInitialChecks: type ${deleteType} with ${ed25519Str(convoId)}`);
const conversation = this.conversations.get(convoId); const conversation = this.conversations.get(convoId);
if (!conversation) { if (!conversation) {
@ -693,7 +694,7 @@ async function leaveClosedGroup(groupPk: PubkeyType | GroupPubkeyType, fromSyncM
window?.log?.info( window?.log?.info(
`We are leaving the group ${ed25519Str(groupPk)}. Sending our leaving messages.` `We are leaving the group ${ed25519Str(groupPk)}. Sending our leaving messages.`
); );
let failedToSent03LeaveMessage = false; let failedToSent03LeaveMessage = true;
// We might not be able to send our leaving messages (no encryption key pair, we were already removed, no network, etc). // We might not be able to send our leaving messages (no encryption key pair, we were already removed, no network, etc).
// If that happens, we should just remove everything from our current user. // If that happens, we should just remove everything from our current user.
try { try {
@ -711,23 +712,28 @@ async function leaveClosedGroup(groupPk: PubkeyType | GroupPubkeyType, fromSyncM
sortedSubRequests: storeRequests, sortedSubRequests: storeRequests,
method: 'sequence', method: 'sequence',
abortSignal: controller.signal, abortSignal: controller.signal,
allow401s: true, // we want "allow" 401s so we don't throw
}), }),
30 * DURATION.SECONDS, 30 * DURATION.SECONDS,
controller controller
); );
if (results?.[0].code !== 200) { if (results?.[0].code === 401) {
window.log.info(
`leaveClosedGroup for ${ed25519Str(groupPk)} failed with 401. Assuming we've been revoked.`
);
} else if (results?.[0].code !== 200) {
throw new Error( throw new Error(
`Even with the retries, leaving message for group ${ed25519Str( `Even with the retries, leaving message for group ${ed25519Str(
groupPk groupPk
)} failed to be sent...` )} failed to be sent...`
); );
} }
failedToSent03LeaveMessage = false;
} catch (e) { } catch (e) {
window?.log?.warn( window?.log?.warn(
`failed to send our leaving messages for ${ed25519Str(groupPk)}:${e.message}` `failed to send our leaving messages for ${ed25519Str(groupPk)}:${e.message}`
); );
failedToSent03LeaveMessage = true;
} }
// the rest of the cleaning of that conversation is done in the `deleteClosedGroup()` // the rest of the cleaning of that conversation is done in the `deleteClosedGroup()`

@ -46,7 +46,7 @@ export async function destroyMessagesAndUpdateRedux(
// TODO make this use getMessagesById and not getMessageById // TODO make this use getMessagesById and not getMessageById
const message = await Data.getMessageById(messageIds[i]); const message = await Data.getMessageById(messageIds[i]);
await message?.cleanup(); await message?.cleanup(false); // not triggering UI updates, as we remove them from the store just below
/* eslint-enable no-await-in-loop */ /* eslint-enable no-await-in-loop */
} }

@ -57,6 +57,7 @@ import { SaveSeenMessageHash, stringify } from '../../types/sqlSharedTypes';
import { OpenGroupRequestCommonType } from '../../data/types'; import { OpenGroupRequestCommonType } from '../../data/types';
import { NetworkTime } from '../../util/NetworkTime'; import { NetworkTime } from '../../util/NetworkTime';
import { MergedAbortSignal } from '../apis/snode_api/requestWith'; import { MergedAbortSignal } from '../apis/snode_api/requestWith';
import { WithAllow401s } from '../types/with';
// ================ SNODE STORE ================ // ================ SNODE STORE ================
@ -420,7 +421,8 @@ async function sendMessagesDataToSnode<T extends PubkeyType | GroupPubkeyType>({
sortedSubRequests, sortedSubRequests,
method, method,
abortSignal, abortSignal,
}: { allow401s,
}: WithAllow401s & {
sortedSubRequests: SortedSubRequestsType<T>; sortedSubRequests: SortedSubRequestsType<T>;
associatedWith: T; associatedWith: T;
method: MethodBatchType; method: MethodBatchType;
@ -448,7 +450,7 @@ async function sendMessagesDataToSnode<T extends PubkeyType | GroupPubkeyType>({
targetNode, targetNode,
timeoutMs: 6 * DURATION.SECONDS, timeoutMs: 6 * DURATION.SECONDS,
associatedWith, associatedWith,
allow401s: false, allow401s,
method, method,
abortSignal, abortSignal,
}); });
@ -473,7 +475,9 @@ async function sendMessagesDataToSnode<T extends PubkeyType | GroupPubkeyType>({
'first result status is not 200 for sendMessagesDataToSnode but: ', 'first result status is not 200 for sendMessagesDataToSnode but: ',
firstResult.code firstResult.code
); );
throw new Error('sendMessagesDataToSnode: Invalid status code'); if (!allow401s || firstResult.code !== 401) {
throw new Error('sendMessagesDataToSnode: Invalid status code');
}
} }
GetNetworkTime.handleTimestampOffsetFromNetwork('store', firstResult.body.t); GetNetworkTime.handleTimestampOffsetFromNetwork('store', firstResult.body.t);
@ -509,7 +513,8 @@ async function sendEncryptedDataToSnode<T extends GroupPubkeyType | PubkeyType>(
sortedSubRequests, sortedSubRequests,
method, method,
abortSignal, abortSignal,
}: { allow401s,
}: WithAllow401s & {
sortedSubRequests: SortedSubRequestsType<T>; // keeping those as an array because the order needs to be enforced for some (group keys for instance) sortedSubRequests: SortedSubRequestsType<T>; // keeping those as an array because the order needs to be enforced for some (group keys for instance)
destination: T; destination: T;
method: MethodBatchType; method: MethodBatchType;
@ -523,6 +528,7 @@ async function sendEncryptedDataToSnode<T extends GroupPubkeyType | PubkeyType>(
associatedWith: destination, associatedWith: destination,
method, method,
abortSignal, abortSignal,
allow401s,
}); });
}, },
{ {

@ -1,4 +1,5 @@
import { PubkeyType } from 'libsession_util_nodejs'; import { PubkeyType } from 'libsession_util_nodejs';
import { Snode } from '../../data/types';
export type WithMessageHash = { messageHash: string }; export type WithMessageHash = { messageHash: string };
export type WithTimestamp = { timestamp: number }; export type WithTimestamp = { timestamp: number };
@ -25,3 +26,11 @@ export type WithLocalMessageDeletionType = { deletionType: 'complete' | 'markDel
export type ShortenOrExtend = 'extend' | 'shorten' | ''; export type ShortenOrExtend = 'extend' | 'shorten' | '';
export type WithShortenOrExtend = { shortenOrExtend: ShortenOrExtend }; export type WithShortenOrExtend = { shortenOrExtend: ShortenOrExtend };
export type WithMessagesHashes = { messagesHashes: Array<string> }; export type WithMessagesHashes = { messagesHashes: Array<string> };
export type WithAllow401s = { allow401s: boolean };
export type WithDestinationEd25519 = { destinationSnodeEd25519: string };
export type WithAssociatedWith = { associatedWith: string | null };
export type WithTargetNode = { targetNode: Snode };
export type WithGuardNode = { guardNode: Snode };
export type WithSymmetricKey = { symmetricKey: ArrayBuffer };

@ -68,7 +68,15 @@ async function addJob({ groupPk, member, inviteAsAdmin, forceUnrevoke }: JobExtr
forceUnrevoke, forceUnrevoke,
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}. inviteAsAdmin:${inviteAsAdmin} `
);
if (inviteAsAdmin) {
// this resets the promotion status to not_sent, so that the sending state can be applied
await MetaGroupWrapperActions.memberSetPromoted(groupPk, member);
} else {
await MetaGroupWrapperActions.memberSetInviteNotSent(groupPk, member);
}
window?.inboxStore?.dispatch( window?.inboxStore?.dispatch(
groupInfoActions.refreshGroupDetailsFromWrapper({ groupPk }) as any groupInfoActions.refreshGroupDetailsFromWrapper({ groupPk }) as any
@ -76,16 +84,6 @@ async function addJob({ groupPk, member, inviteAsAdmin, forceUnrevoke }: JobExtr
await LibSessionUtil.saveDumpsToDb(groupPk); await LibSessionUtil.saveDumpsToDb(groupPk);
await runners.groupInviteJobRunner.addJob(groupInviteJob); await runners.groupInviteJobRunner.addJob(groupInviteJob);
if (inviteAsAdmin) {
window?.inboxStore?.dispatch(
groupInfoActions.setPromotionPending({ groupPk, pubkey: member, sending: true })
);
} else {
window?.inboxStore?.dispatch(
groupInfoActions.setInvitePending({ groupPk, pubkey: member, sending: true })
);
}
} }
} }
@ -200,6 +198,7 @@ class GroupInviteJob extends PersistedJob<GroupInvitePersistedData> {
groupPk, groupPk,
unrevokeSubRequest, unrevokeSubRequest,
extraStoreRequests: [], extraStoreRequests: [],
allow401s: false,
}); });
if (sequenceResult !== RunJobResult.Success) { if (sequenceResult !== RunJobResult.Success) {
await LibSessionUtil.saveDumpsToDb(groupPk); await LibSessionUtil.saveDumpsToDb(groupPk);
@ -278,15 +277,6 @@ class GroupInviteJob extends PersistedJob<GroupInvitePersistedData> {
updateFailedStateForMember(groupPk, member, failed); updateFailedStateForMember(groupPk, member, failed);
if (inviteAsAdmin) {
window?.inboxStore?.dispatch(
groupInfoActions.setPromotionPending({ groupPk, pubkey: member, sending: false })
);
} else {
window?.inboxStore?.dispatch(
groupInfoActions.setInvitePending({ groupPk, pubkey: member, sending: false })
);
}
window?.inboxStore?.dispatch( window?.inboxStore?.dispatch(
groupInfoActions.refreshGroupDetailsFromWrapper({ groupPk }) as any groupInfoActions.refreshGroupDetailsFromWrapper({ groupPk }) as any
); );

@ -68,13 +68,10 @@ async function getPendingRevokeParams({
const revokeChanges: RevokeChanges = []; const revokeChanges: RevokeChanges = [];
const unrevokeChanges: RevokeChanges = []; const unrevokeChanges: RevokeChanges = [];
for (let index = 0; index < withoutHistory.length; index++) { const toUnrevoke = withoutHistory.concat(withHistory);
const m = withoutHistory[index];
const token = await MetaGroupWrapperActions.swarmSubAccountToken(groupPk, m); for (let index = 0; index < toUnrevoke.length; index++) {
unrevokeChanges.push({ action: 'unrevoke_subaccount', tokenToRevokeHex: token }); const m = toUnrevoke[index];
}
for (let index = 0; index < withHistory.length; index++) {
const m = withHistory[index];
const token = await MetaGroupWrapperActions.swarmSubAccountToken(groupPk, m); const token = await MetaGroupWrapperActions.swarmSubAccountToken(groupPk, m);
unrevokeChanges.push({ action: 'unrevoke_subaccount', tokenToRevokeHex: token }); unrevokeChanges.push({ action: 'unrevoke_subaccount', tokenToRevokeHex: token });
} }
@ -198,6 +195,7 @@ class GroupPendingRemovalsJob extends PersistedJob<GroupPendingRemovalsPersisted
destination: groupPk, destination: groupPk,
method: 'sequence', method: 'sequence',
abortSignal: controller.signal, abortSignal: controller.signal,
allow401s: false,
}), }),
30 * DURATION.SECONDS, 30 * DURATION.SECONDS,
controller controller

@ -46,7 +46,7 @@ async function addJob({ groupPk, member }: JobExtraArgs) {
window.log.debug(`addGroupPromoteJob: adding group promote for ${groupPk}:${member} `); window.log.debug(`addGroupPromoteJob: adding group promote for ${groupPk}:${member} `);
await runners.groupPromoteJobRunner.addJob(groupPromoteJob); await runners.groupPromoteJobRunner.addJob(groupPromoteJob);
window?.inboxStore?.dispatch( window?.inboxStore?.dispatch(
groupInfoActions.setPromotionPending({ groupPk, pubkey: member, sending: true }) groupInfoActions.refreshGroupDetailsFromWrapper({ groupPk }) as any
); );
} }
} }
@ -111,9 +111,6 @@ class GroupPromoteJob extends PersistedJob<GroupPromotePersistedData> {
failed = false; failed = false;
} }
} finally { } finally {
window?.inboxStore?.dispatch(
groupInfoActions.setPromotionPending({ groupPk, pubkey: member, sending: false })
);
try { try {
if (failed) { if (failed) {
await MetaGroupWrapperActions.memberSetPromotionFailed(groupPk, member); await MetaGroupWrapperActions.memberSetPromotionFailed(groupPk, member);

@ -36,6 +36,7 @@ import {
RunJobResult, RunJobResult,
} from '../PersistedJob'; } from '../PersistedJob';
import { DURATION } from '../../../constants'; import { DURATION } from '../../../constants';
import { WithAllow401s } from '../../../types/with';
const defaultMsBetweenRetries = 15000; // a long time between retries, to avoid running multiple jobs at the same time, when one was postponed at the same time as one already planned (5s) const defaultMsBetweenRetries = 15000; // a long time between retries, to avoid running multiple jobs at the same time, when one was postponed at the same time as one already planned (5s)
const defaultMaxAttempts = 2; const defaultMaxAttempts = 2;
@ -89,7 +90,9 @@ async function pushChangesToGroupSwarmIfNeeded({
supplementalKeysSubRequest, supplementalKeysSubRequest,
deleteAllMessagesSubRequest, deleteAllMessagesSubRequest,
extraStoreRequests, extraStoreRequests,
allow401s,
}: WithGroupPubkey & }: WithGroupPubkey &
WithAllow401s &
WithRevokeSubRequest & { WithRevokeSubRequest & {
supplementalKeysSubRequest?: StoreGroupKeysSubRequest; supplementalKeysSubRequest?: StoreGroupKeysSubRequest;
deleteAllMessagesSubRequest?: DeleteAllFromGroupMsgNodeSubRequest; deleteAllMessagesSubRequest?: DeleteAllFromGroupMsgNodeSubRequest;
@ -170,6 +173,7 @@ async function pushChangesToGroupSwarmIfNeeded({
destination: groupPk, destination: groupPk,
method: 'sequence', method: 'sequence',
abortSignal: controller.signal, abortSignal: controller.signal,
allow401s,
}), }),
30 * DURATION.SECONDS, 30 * DURATION.SECONDS,
controller controller
@ -250,6 +254,7 @@ class GroupSyncJob extends PersistedJob<GroupSyncPersistedData> {
return await GroupSync.pushChangesToGroupSwarmIfNeeded({ return await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk: thisJobDestination, groupPk: thisJobDestination,
extraStoreRequests: [], extraStoreRequests: [],
allow401s: false,
}); });
} catch (e) { } catch (e) {
window.log.warn('GroupSyncJob failed with', e.message); window.log.warn('GroupSyncJob failed with', e.message);

@ -126,6 +126,7 @@ async function pushChangesToUserSwarmIfNeeded() {
destination: us, destination: us,
method: 'sequence', method: 'sequence',
abortSignal: controller.signal, abortSignal: controller.signal,
allow401s: false, // user swarm push, we shouldn't need to allow 401s
}), }),
30 * DURATION.SECONDS, 30 * DURATION.SECONDS,
controller controller

@ -7,7 +7,6 @@ import {
PubkeyType, PubkeyType,
UserGroupsGet, UserGroupsGet,
WithGroupPubkey, WithGroupPubkey,
WithPubkey,
} from 'libsession_util_nodejs'; } from 'libsession_util_nodejs';
import { concat, intersection, isEmpty, uniq } from 'lodash'; import { concat, intersection, isEmpty, uniq } from 'lodash';
import { from_hex } from 'libsodium-wrappers-sumo'; import { from_hex } from 'libsodium-wrappers-sumo';
@ -225,6 +224,7 @@ const initNewGroupInWrapper = createAsyncThunk(
const result = await GroupSync.pushChangesToGroupSwarmIfNeeded({ const result = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk, groupPk,
extraStoreRequests, extraStoreRequests,
allow401s: false,
}); });
if (result !== RunJobResult.Success) { if (result !== RunJobResult.Success) {
window.log.warn('GroupSync.pushChangesToGroupSwarmIfNeeded during create failed'); window.log.warn('GroupSync.pushChangesToGroupSwarmIfNeeded during create failed');
@ -423,6 +423,10 @@ const refreshGroupDetailsFromWrapper = createAsyncThunk(
const infos = await MetaGroupWrapperActions.infoGet(groupPk); const infos = await MetaGroupWrapperActions.infoGet(groupPk);
const members = await MetaGroupWrapperActions.memberGetAll(groupPk); const members = await MetaGroupWrapperActions.memberGetAll(groupPk);
if (window.sessionFeatureFlags.debug.debugLibsessionDumps) {
window.log.info(`groupInfo after refreshGroupDetailsFromWrapper: ${stringify(infos)}`);
window.log.info(`groupMembers after refreshGroupDetailsFromWrapper: ${stringify(members)}`);
}
return { groupPk, infos, members }; return { groupPk, infos, members };
} catch (e) { } catch (e) {
window.log.warn('refreshGroupDetailsFromWrapper failed with ', e.message); window.log.warn('refreshGroupDetailsFromWrapper failed with ', e.message);
@ -670,6 +674,7 @@ async function handleMemberAddedFromUI({
revokeSubRequest, revokeSubRequest,
unrevokeSubRequest, unrevokeSubRequest,
extraStoreRequests, extraStoreRequests,
allow401s: false,
}); });
if (sequenceResult !== RunJobResult.Success) { if (sequenceResult !== RunJobResult.Success) {
await LibSessionUtil.saveDumpsToDb(groupPk); await LibSessionUtil.saveDumpsToDb(groupPk);
@ -747,6 +752,7 @@ async function handleMemberRemovedFromUI({
// We don't revoke the member's token right away. Instead we schedule a `GroupPendingRemovals` // We don't revoke the member's token right away. Instead we schedule a `GroupPendingRemovals`
// which will deal with the revokes of all of them together. // which will deal with the revokes of all of them together.
await GroupPendingRemovals.addJob({ groupPk }); await GroupPendingRemovals.addJob({ groupPk });
window.inboxStore?.dispatch(refreshGroupDetailsFromWrapper({ groupPk }) as any);
// Build a GroupUpdateMessage to be sent if that member was kicked by us. // Build a GroupUpdateMessage to be sent if that member was kicked by us.
const createAtNetworkTimestamp = NetworkTime.now(); const createAtNetworkTimestamp = NetworkTime.now();
@ -795,6 +801,7 @@ async function handleMemberRemovedFromUI({
const sequenceResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({ const sequenceResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk, groupPk,
extraStoreRequests, extraStoreRequests,
allow401s: false,
}); });
if (sequenceResult !== RunJobResult.Success) { if (sequenceResult !== RunJobResult.Success) {
throw new Error( throw new Error(
@ -880,6 +887,7 @@ async function handleNameChangeFromUI({
const batchResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({ const batchResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk, groupPk,
extraStoreRequests, extraStoreRequests,
allow401s: false,
}); });
if (batchResult !== RunJobResult.Success) { if (batchResult !== RunJobResult.Success) {
@ -1005,6 +1013,7 @@ const triggerFakeAvatarUpdate = createAsyncThunk(
const batchResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({ const batchResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk, groupPk,
extraStoreRequests, extraStoreRequests,
allow401s: false,
}); });
if (!batchResult) { if (!batchResult) {
window.log.warn(`failed to send avatarChange message for group ${ed25519Str(groupPk)}`); window.log.warn(`failed to send avatarChange message for group ${ed25519Str(groupPk)}`);
@ -1060,6 +1069,7 @@ const triggerFakeDeleteMsgBeforeNow = createAsyncThunk(
const batchResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({ const batchResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk, groupPk,
extraStoreRequests, extraStoreRequests,
allow401s: false,
}); });
if (!batchResult) { if (!batchResult) {
window.log.warn( window.log.warn(
@ -1206,36 +1216,6 @@ function deleteGroupPkEntriesFromState(state: GroupState, groupPk: GroupPubkeyTy
delete state.members[groupPk]; delete state.members[groupPk];
} }
function applySendingStateChange({
groupPk,
pubkey,
sending,
state,
changeType,
}: WithGroupPubkey &
WithPubkey & { sending: boolean; changeType: 'invite' | 'promote'; state: GroupState }) {
if (changeType === 'invite' && !state.membersInviteSending[groupPk]) {
state.membersInviteSending[groupPk] = [];
} else if (changeType === 'promote' && !state.membersPromoteSending[groupPk]) {
state.membersPromoteSending[groupPk] = [];
}
const arrRef =
changeType === 'invite'
? state.membersInviteSending[groupPk]
: state.membersPromoteSending[groupPk];
const foundAt = arrRef.findIndex(p => p === pubkey);
if (sending && foundAt === -1) {
arrRef.push(pubkey);
return state;
}
if (!sending && foundAt >= 0) {
arrRef.splice(foundAt, 1);
}
return state;
}
function refreshConvosModelProps(convoIds: Array<string>) { function refreshConvosModelProps(convoIds: Array<string>) {
/** /**
* *
@ -1257,19 +1237,6 @@ const metaGroupSlice = createSlice({
name: 'metaGroup', name: 'metaGroup',
initialState: initialGroupState, initialState: initialGroupState,
reducers: { reducers: {
setInvitePending(
state: GroupState,
{ payload }: PayloadAction<{ sending: boolean } & WithGroupPubkey & WithPubkey>
) {
return applySendingStateChange({ changeType: 'invite', ...payload, state });
},
setPromotionPending(
state: GroupState,
{ payload }: PayloadAction<{ pubkey: PubkeyType; groupPk: GroupPubkeyType; sending: boolean }>
) {
return applySendingStateChange({ changeType: 'promote', ...payload, state });
},
removeGroupDetailsFromSlice( removeGroupDetailsFromSlice(
state: GroupState, state: GroupState,
{ payload }: PayloadAction<{ groupPk: GroupPubkeyType }> { payload }: PayloadAction<{ groupPk: GroupPubkeyType }>

@ -272,19 +272,27 @@ export function useStateOf03GroupMembers(convoId?: string) {
); );
const sorted = useMemo(() => { const sorted = useMemo(() => {
// needing an index like this outside of lodash is not pretty, // damn this is overkill
// but sortBy doesn't provide the index in the callback return sortBy(
let index = 0; unsortedMembers,
return sortBy(unsortedMembers, item => { item => {
const stateSortingOrder = getSortingOrderForStatus(item.memberStatus); const sortingOrder = getSortingOrderForStatus(item.memberStatus);
const sortingOrder = [ return sortingOrder;
stateSortingOrder, },
item => {
// per section, we want "us" first, then "nickname || displayName || pubkey" // per section, we want "us" first, then "nickname || displayName || pubkey"
item.pubkeyHex === us ? -1 : names[index],
]; if (item.pubkeyHex === us) {
index++; return -1;
return sortingOrder; }
}); const index = unsortedMembers.findIndex(p => p.pubkeyHex === item.pubkeyHex);
if (index < 0 || index >= names.length) {
throw new Error('this should never happen');
}
return names[index].toLowerCase();
}
);
}, [unsortedMembers, us, names]); }, [unsortedMembers, us, names]);
return sorted; return sorted;
} }

@ -1,5 +1,6 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useSelector } from 'react-redux';
import { LeftOverlayMode, SectionStateType, SectionType } from '../ducks/section'; import { LeftOverlayMode, SectionStateType, SectionType } from '../ducks/section';
import { StateType } from '../reducer'; import { StateType } from '../reducer';
import type { SessionSettingCategory } from '../../types/ReduxTypes'; import type { SessionSettingCategory } from '../../types/ReduxTypes';
@ -34,9 +35,13 @@ export const getRightOverlayMode = (state: StateType) => {
return state.section.rightOverlayMode; return state.section.rightOverlayMode;
}; };
export const getIsMessageRequestOverlayShown = (state: StateType) => { const getIsMessageRequestOverlayShown = (state: StateType) => {
const focusedSection = getFocusedSection(state); const focusedSection = getFocusedSection(state);
const leftOverlayMode = getLeftOverlayMode(state); const leftOverlayMode = getLeftOverlayMode(state);
return focusedSection === SectionType.Message && leftOverlayMode === 'message-requests'; return focusedSection === SectionType.Message && leftOverlayMode === 'message-requests';
}; };
export function useIsMessageRequestOverlayShown() {
return useSelector(getIsMessageRequestOverlayShown);
}

@ -297,6 +297,7 @@ describe('GroupSyncJob pushChangesToGroupSwarmIfNeeded', () => {
const result = await GroupSync.pushChangesToGroupSwarmIfNeeded({ const result = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk, groupPk,
extraStoreRequests: [], extraStoreRequests: [],
allow401s: false,
}); });
expect(result).to.be.eq(RunJobResult.Success); expect(result).to.be.eq(RunJobResult.Success);
expect(sendStub.callCount).to.be.eq(0); expect(sendStub.callCount).to.be.eq(0);
@ -322,6 +323,7 @@ describe('GroupSyncJob pushChangesToGroupSwarmIfNeeded', () => {
const result = await GroupSync.pushChangesToGroupSwarmIfNeeded({ const result = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk, groupPk,
extraStoreRequests: [], extraStoreRequests: [],
allow401s: false,
}); });
sendStub.resolves(undefined); sendStub.resolves(undefined);
@ -369,6 +371,7 @@ describe('GroupSyncJob pushChangesToGroupSwarmIfNeeded', () => {
const result = await GroupSync.pushChangesToGroupSwarmIfNeeded({ const result = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk, groupPk,
extraStoreRequests: [], extraStoreRequests: [],
allow401s: false,
}); });
expect(sendStub.callCount).to.be.eq(1); expect(sendStub.callCount).to.be.eq(1);

@ -4944,9 +4944,9 @@ levn@~0.3.0:
prelude-ls "~1.1.2" prelude-ls "~1.1.2"
type-check "~0.3.2" type-check "~0.3.2"
"libsession_util_nodejs@https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.4.10/libsession_util_nodejs-v0.4.10.tar.gz": "libsession_util_nodejs@https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.4.11/libsession_util_nodejs-v0.4.11.tar.gz":
version "0.4.10" version "0.4.11"
resolved "https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.4.10/libsession_util_nodejs-v0.4.10.tar.gz#9a420fa0ad4dc9067de17b2ec9fa3676a1c10056" resolved "https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.4.11/libsession_util_nodejs-v0.4.11.tar.gz#ea6ab3fc11088ede3c79ddc5b702b17af842f774"
dependencies: dependencies:
cmake-js "7.2.1" cmake-js "7.2.1"
node-addon-api "^6.1.0" node-addon-api "^6.1.0"

Loading…
Cancel
Save