feat: add the deleteContact and deleteConversation only menu items

pull/2620/head
Audric Ackermann 3 years ago
parent 760ce5caa5
commit 2a4bbbd587

@ -100,6 +100,7 @@
"deleteMessagesQuestion": "Delete $count$ messages?", "deleteMessagesQuestion": "Delete $count$ messages?",
"deleteMessageQuestion": "Delete this message?", "deleteMessageQuestion": "Delete this message?",
"deleteMessages": "Delete Messages", "deleteMessages": "Delete Messages",
"deleteConversation": "Delete Conversation",
"deleted": "$count$ deleted", "deleted": "$count$ deleted",
"messageDeletedPlaceholder": "This message has been deleted", "messageDeletedPlaceholder": "This message has been deleted",
"from": "From:", "from": "From:",

@ -29,7 +29,9 @@ export const AdminLeaveClosedGroupDialog = (props: { conversationId: string }) =
} }
setLoading(true); setLoading(true);
// we know want to delete a closed group right after we've left it, so we can call the deleteContact which takes care of it all // we know want to delete a closed group right after we've left it, so we can call the deleteContact which takes care of it all
await getConversationController().deleteContact(props.conversationId, false); await getConversationController().deleteContact(props.conversationId, {
fromSyncMessage: false,
});
setLoading(false); setLoading(false);
closeDialog(); closeDialog();
}; };

@ -29,7 +29,8 @@ import {
BlockMenuItem, BlockMenuItem,
ChangeNicknameMenuItem, ChangeNicknameMenuItem,
ClearNicknameMenuItem, ClearNicknameMenuItem,
DeleteContactMenuItem, DeletePrivateContactMenuItem,
DeleteGroupOrCommunityMenuItem,
DeleteMessagesMenuItem, DeleteMessagesMenuItem,
InviteContactMenuItem, InviteContactMenuItem,
LeaveGroupMenuItem, LeaveGroupMenuItem,
@ -38,6 +39,7 @@ import {
ShowUserDetailsMenuItem, ShowUserDetailsMenuItem,
UnbanMenuItem, UnbanMenuItem,
UpdateGroupNameMenuItem, UpdateGroupNameMenuItem,
DeletePrivateConversationMenuItem,
} from './Menu'; } from './Menu';
import { ContextConversationProvider } from '../leftpane/conversation-list-item/ConvoIdContext'; import { ContextConversationProvider } from '../leftpane/conversation-list-item/ConvoIdContext';
@ -71,7 +73,6 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => {
<MarkAllReadMenuItem /> <MarkAllReadMenuItem />
<ChangeNicknameMenuItem /> <ChangeNicknameMenuItem />
<ClearNicknameMenuItem /> <ClearNicknameMenuItem />
<DeleteMessagesMenuItem />
<AddModeratorsMenuItem /> <AddModeratorsMenuItem />
<RemoveModeratorsMenuItem /> <RemoveModeratorsMenuItem />
<BanMenuItem /> <BanMenuItem />
@ -79,7 +80,10 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => {
<UpdateGroupNameMenuItem /> <UpdateGroupNameMenuItem />
<LeaveGroupMenuItem /> <LeaveGroupMenuItem />
<InviteContactMenuItem /> <InviteContactMenuItem />
<DeleteContactMenuItem /> <DeleteMessagesMenuItem />
<DeletePrivateConversationMenuItem />
<DeletePrivateContactMenuItem />
<DeleteGroupOrCommunityMenuItem />
<ShowUserDetailsMenuItem /> <ShowUserDetailsMenuItem />
</Menu> </Menu>
</SessionContextMenuContainer> </SessionContextMenuContainer>

@ -17,7 +17,8 @@ import {
CopyMenuItem, CopyMenuItem,
DeclineAndBlockMsgRequestMenuItem, DeclineAndBlockMsgRequestMenuItem,
DeclineMsgRequestMenuItem, DeclineMsgRequestMenuItem,
DeleteContactMenuItem, DeletePrivateContactMenuItem,
DeleteGroupOrCommunityMenuItem,
DeleteMessagesMenuItem, DeleteMessagesMenuItem,
InviteContactMenuItem, InviteContactMenuItem,
LeaveGroupMenuItem, LeaveGroupMenuItem,
@ -25,6 +26,7 @@ import {
MarkConversationUnreadMenuItem, MarkConversationUnreadMenuItem,
ShowUserDetailsMenuItem, ShowUserDetailsMenuItem,
UnbanMenuItem, UnbanMenuItem,
DeletePrivateConversationMenuItem,
} from './Menu'; } from './Menu';
export type PropsContextConversationItem = { export type PropsContextConversationItem = {
@ -48,7 +50,6 @@ const ConversationListItemContextMenu = (props: PropsContextConversationItem) =>
{/* Read state actions */} {/* Read state actions */}
<MarkAllReadMenuItem /> <MarkAllReadMenuItem />
<MarkConversationUnreadMenuItem /> <MarkConversationUnreadMenuItem />
<DeleteMessagesMenuItem />
{/* Nickname actions */} {/* Nickname actions */}
<ChangeNicknameMenuItem /> <ChangeNicknameMenuItem />
<ClearNicknameMenuItem /> <ClearNicknameMenuItem />
@ -56,7 +57,10 @@ const ConversationListItemContextMenu = (props: PropsContextConversationItem) =>
<BanMenuItem /> <BanMenuItem />
<UnbanMenuItem /> <UnbanMenuItem />
<InviteContactMenuItem /> <InviteContactMenuItem />
<DeleteContactMenuItem /> <DeleteMessagesMenuItem />
<DeletePrivateConversationMenuItem />
<DeletePrivateContactMenuItem />
<DeleteGroupOrCommunityMenuItem />
<LeaveGroupMenuItem /> <LeaveGroupMenuItem />
<ShowUserDetailsMenuItem /> <ShowUserDetailsMenuItem />
</Menu> </Menu>

@ -35,6 +35,7 @@ import {
unblockConvoById, unblockConvoById,
} from '../../interactions/conversationInteractions'; } from '../../interactions/conversationInteractions';
import { getConversationController } from '../../session/conversations'; import { getConversationController } from '../../session/conversations';
import { PubKey } from '../../session/types';
import { import {
changeNickNameModal, changeNickNameModal,
updateConfirmModal, updateConfirmModal,
@ -44,39 +45,6 @@ import { getIsMessageSection } 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';
import { useConvoIdFromContext } from '../leftpane/conversation-list-item/ConvoIdContext'; import { useConvoIdFromContext } from '../leftpane/conversation-list-item/ConvoIdContext';
import { PubKey } from '../../session/types';
function showDeleteContact(
isGroup: boolean,
isPublic: boolean,
isGroupLeft: boolean,
isKickedFromGroup: boolean,
isRequest: boolean
): boolean {
// you need to have left a closed group first to be able to delete it completely.
return (!isGroup && !isRequest) || (isGroup && (isGroupLeft || isKickedFromGroup || isPublic));
}
function showUpdateGroupName(
weAreAdmin: boolean,
isKickedFromGroup: boolean,
left: boolean
): boolean {
return !isKickedFromGroup && !left && weAreAdmin;
}
function showLeaveGroup(
isKickedFromGroup: boolean,
left: boolean,
isGroup: boolean,
isPublic: boolean
): boolean {
return !isKickedFromGroup && !left && isGroup && !isPublic;
}
function showInviteContact(isPublic: boolean): boolean {
return isPublic;
}
/** Menu items standardized */ /** Menu items standardized */
@ -84,7 +52,7 @@ export const InviteContactMenuItem = (): JSX.Element | null => {
const convoId = useConvoIdFromContext(); const convoId = useConvoIdFromContext();
const isPublic = useIsPublic(convoId); const isPublic = useIsPublic(convoId);
if (showInviteContact(isPublic)) { if (isPublic) {
return ( return (
<Item <Item
onClick={() => { onClick={() => {
@ -116,24 +84,61 @@ export const MarkConversationUnreadMenuItem = (): JSX.Element | null => {
return null; return null;
}; };
export const DeleteContactMenuItem = () => { /**
* This menu item can be used to completely remove a contact and reset the flags of that conversation.
* i.e. after confirmation is made, this contact will be removed from the ContactWrapper, and its blocked and approved state reset.
* Note: We keep the entry in the database as the user profile might still be needed for communities/groups where this user.
*/
export const DeletePrivateContactMenuItem = () => {
const dispatch = useDispatch();
const convoId = useConvoIdFromContext();
const isPrivate = useIsPrivate(convoId);
const isRequest = useIsIncomingRequest(convoId);
if (isPrivate && !isRequest) {
let menuItemText: string;
menuItemText = window.i18n('editMenuDeleteContact');
const onClickClose = () => {
dispatch(updateConfirmModal(null));
};
const showConfirmationModal = () => {
dispatch(
updateConfirmModal({
title: menuItemText,
message: window.i18n('deleteContactConfirmation'),
onClickClose,
okTheme: SessionButtonColor.Danger,
onClickOk: async () => {
await getConversationController().deleteContact(convoId, {
fromSyncMessage: false,
justHidePrivate: false,
});
},
})
);
};
return <Item onClick={showConfirmationModal}>{menuItemText}</Item>;
}
return null;
};
export const DeleteGroupOrCommunityMenuItem = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const convoId = useConvoIdFromContext(); const convoId = useConvoIdFromContext();
const isPublic = useIsPublic(convoId); const isPublic = useIsPublic(convoId);
const isLeft = useIsLeft(convoId); const isLeft = useIsLeft(convoId);
const isKickedFromGroup = useIsKickedFromGroup(convoId); const isKickedFromGroup = useIsKickedFromGroup(convoId);
const isPrivate = useIsPrivate(convoId); const isPrivate = useIsPrivate(convoId);
const isRequest = useIsIncomingRequest(convoId); const isGroup = !isPrivate && !isPublic;
if (showDeleteContact(!isPrivate, isPublic, isLeft, isKickedFromGroup, isRequest)) { // You need to have left a closed group first to be able to delete it completely as there is a leaving message to send first.
let menuItemText: string; // A community can just be removed right away.
if (isPublic) { if (isPublic || (isGroup && (isLeft || isKickedFromGroup))) {
menuItemText = window.i18n('leaveGroup'); const menuItemText = isPublic ? window.i18n('leaveGroup') : window.i18n('editMenuDeleteGroup');
} else {
menuItemText = isPrivate
? window.i18n('editMenuDeleteContact')
: window.i18n('editMenuDeleteGroup');
}
const onClickClose = () => { const onClickClose = () => {
dispatch(updateConfirmModal(null)); dispatch(updateConfirmModal(null));
@ -143,13 +148,13 @@ export const DeleteContactMenuItem = () => {
dispatch( dispatch(
updateConfirmModal({ updateConfirmModal({
title: menuItemText, title: menuItemText,
message: isPrivate message: window.i18n('leaveGroupConfirmation'),
? window.i18n('deleteContactConfirmation')
: window.i18n('leaveGroupConfirmation'),
onClickClose, onClickClose,
okTheme: SessionButtonColor.Danger, okTheme: SessionButtonColor.Danger,
onClickOk: async () => { onClickOk: async () => {
await getConversationController().deleteContact(convoId, false); await getConversationController().deleteContact(convoId, {
fromSyncMessage: false,
});
}, },
}) })
); );
@ -167,7 +172,7 @@ export const LeaveGroupMenuItem = () => {
const isKickedFromGroup = useIsKickedFromGroup(convoId); const isKickedFromGroup = useIsKickedFromGroup(convoId);
const isPrivate = useIsPrivate(convoId); const isPrivate = useIsPrivate(convoId);
if (showLeaveGroup(isKickedFromGroup, isLeft, !isPrivate, isPublic)) { if (!isKickedFromGroup && !isLeft && !isPrivate && !isPublic) {
return ( return (
<Item <Item
onClick={() => { onClick={() => {
@ -217,7 +222,7 @@ export const UpdateGroupNameMenuItem = () => {
const isKickedFromGroup = useIsKickedFromGroup(convoId); const isKickedFromGroup = useIsKickedFromGroup(convoId);
const weAreAdmin = useWeAreAdmin(convoId); const weAreAdmin = useWeAreAdmin(convoId);
if (showUpdateGroupName(weAreAdmin, isKickedFromGroup, left)) { if (!isKickedFromGroup && !left && weAreAdmin) {
return ( return (
<Item <Item
onClick={async () => { onClick={async () => {
@ -406,13 +411,17 @@ export const ChangeNicknameMenuItem = () => {
); );
}; };
/**
* This menu is always available and can be used to clear the messages in the local database only.
* No messages are sent, no update are made in the wrappers.
* Note: Will ask for confirmation before processing.
*/
export const DeleteMessagesMenuItem = () => { export const DeleteMessagesMenuItem = () => {
const convoId = useConvoIdFromContext(); const convoId = useConvoIdFromContext();
if (!convoId) { if (!convoId) {
return null; return null;
} }
return ( return (
<Item <Item
onClick={() => { onClick={() => {
@ -424,6 +433,34 @@ export const DeleteMessagesMenuItem = () => {
); );
}; };
/**
* This menu item can be used to delete a private conversation after confirmation.
* It does not reset the flags of that conversation, but just removes the messages locally and hide it from the left pane list.
* Note: A dialog is opened to ask for confirmation before processing.
*/
export const DeletePrivateConversationMenuItem = () => {
const convoId = useConvoIdFromContext();
const isRequest = useIsIncomingRequest(convoId);
const isPrivate = useIsPrivate(convoId);
if (!convoId || !isPrivate || isRequest) {
return null;
}
return (
<Item
onClick={async () => {
await getConversationController().deleteContact(convoId, {
fromSyncMessage: false,
justHidePrivate: true,
});
}}
>
{window.i18n('deleteConversation')}
</Item>
);
};
export const AcceptMsgRequestMenuItem = () => { export const AcceptMsgRequestMenuItem = () => {
const convoId = useConvoIdFromContext(); const convoId = useConvoIdFromContext();
const isRequest = useIsIncomingRequest(convoId); const isRequest = useIsIncomingRequest(convoId);

@ -249,7 +249,9 @@ export function showLeaveGroupByConvoId(conversationId: string) {
title, title,
message, message,
onClickOk: async () => { onClickOk: async () => {
await getConversationController().deleteContact(conversation.id, false); await getConversationController().deleteContact(conversation.id, {
fromSyncMessage: false,
});
onClickClose(); onClickClose();
}, },
onClickClose, onClickClose,

@ -162,7 +162,7 @@ Storage.onready(async () => {
// Stop background processing // Stop background processing
AttachmentDownloads.stop(); AttachmentDownloads.stop();
// Stop processing incoming messages // Stop processing incoming messages
// TODO stop polling opengroupv2 and swarm nodes // TODOLATER stop polling opengroupv2 and swarm nodes
// Shut down the data interface cleanly // Shut down the data interface cleanly
await Data.shutdown(); await Data.shutdown();

@ -595,7 +595,7 @@ function getUsBlindedInThatServerIfNeeded(
const blindedId = found?.blindedId; const blindedId = found?.blindedId;
return isString(blindedId) ? blindedId : usNaked; return isString(blindedId) ? blindedId : usNaked;
} catch (e) { } catch (e) {
console.warn('getUsBlindedInThatServerIfNeeded failed with ', e.message); console.error('getUsBlindedInThatServerIfNeeded failed with ', e.message);
} }
return usNaked; return usNaked;
@ -1621,7 +1621,7 @@ const unprocessed: UnprocessedDataNode = {
removeUnprocessed: (id: string): void => { removeUnprocessed: (id: string): void => {
if (Array.isArray(id)) { if (Array.isArray(id)) {
console.warn('removeUnprocessed only supports single ids at a time'); console.error('removeUnprocessed only supports single ids at a time');
throw new Error('removeUnprocessed only supports single ids at a time'); throw new Error('removeUnprocessed only supports single ids at a time');
} }
assertGlobalInstance() assertGlobalInstance()

@ -1,4 +1,4 @@
import { compact, isEmpty, isNumber, toNumber } from 'lodash'; import { compact, difference, isEmpty, isNumber, toNumber } from 'lodash';
import { ConfigDumpData } from '../data/configDump/configDump'; import { ConfigDumpData } from '../data/configDump/configDump';
import { Data } from '../data/data'; import { Data } from '../data/data';
import { SettingsKey } from '../data/settings-key'; import { SettingsKey } from '../data/settings-key';
@ -47,6 +47,9 @@ import { addKeyPairToCacheAndDBIfNeeded, handleNewClosedGroup } from './closedGr
import { HexKeyPair } from './keypairs'; import { HexKeyPair } from './keypairs';
import { queueAllCachedFromSource } from './receiver'; import { queueAllCachedFromSource } from './receiver';
import { EnvelopePlus } from './types'; import { EnvelopePlus } from './types';
import { SessionUtilContact } from '../session/utils/libsession/libsession_utils_contacts';
import { ContactInfo } from 'libsession_util_nodejs';
import { getCurrentlySelectedConversationOutsideRedux } from '../state/selectors/conversations';
function groupByVariant( function groupByVariant(
incomingConfigs: Array<IncomingMessage<SignalService.ISharedConfigMessage>> incomingConfigs: Array<IncomingMessage<SignalService.ISharedConfigMessage>>
@ -136,14 +139,77 @@ async function handleUserProfileUpdate(result: IncomingConfResult): Promise<Inco
return result; return result;
} }
function getContactsToRemoveFromDB(contactsInWrapper: Array<ContactInfo>) {
const allContactsInDBWhichShouldBeInWrapperIds = getConversationController()
.getConversations()
.filter(SessionUtilContact.isContactToStoreInWrapper)
.map(m => m.id as string);
const currentlySelectedConversationId = getCurrentlySelectedConversationOutsideRedux();
const currentlySelectedConvo = currentlySelectedConversationId
? getConversationController().get(currentlySelectedConversationId)
: undefined;
// we might have some contacts not in the wrapper anymore, so let's clean things up.
const convoIdsInDbButNotWrapper = difference(
allContactsInDBWhichShouldBeInWrapperIds,
contactsInWrapper.map(m => m.id)
);
// When starting a conversation with a new user, it is not in the wrapper yet, only when we send the first message.
// We do not want to forcefully remove that contact as the user might be typing a message to him.
// So let's check if that currently selected conversation should be forcefully closed or not
if (
currentlySelectedConversationId &&
currentlySelectedConvo &&
convoIdsInDbButNotWrapper.includes(currentlySelectedConversationId)
) {
if (
currentlySelectedConvo.isPrivate() &&
!currentlySelectedConvo.isApproved() &&
!currentlySelectedConvo.didApproveMe()
) {
const foundIndex = convoIdsInDbButNotWrapper.findIndex(
m => m === currentlySelectedConversationId
);
if (foundIndex !== -1) {
convoIdsInDbButNotWrapper.splice(foundIndex, 1);
}
}
}
return convoIdsInDbButNotWrapper;
}
async function deleteContactsFromDB(contactsToRemove: Array<string>) {
window.log.debug('contacts to fully remove after wrapper merge', contactsToRemove);
for (let index = 0; index < contactsToRemove.length; index++) {
const contactToRemove = contactsToRemove[index];
try {
await getConversationController().deleteContact(contactToRemove, {
fromSyncMessage: true,
justHidePrivate: false,
});
} catch (e) {
window.log.warn(
`after merge: deleteContactsFromDB ${contactToRemove} failed with `,
e.message
);
}
}
}
// tslint:disable-next-line: cyclomatic-complexity // tslint:disable-next-line: cyclomatic-complexity
async function handleContactsUpdate(result: IncomingConfResult): Promise<IncomingConfResult> { async function handleContactsUpdate(result: IncomingConfResult): Promise<IncomingConfResult> {
const us = UserUtils.getOurPubKeyStrFromCache(); const us = UserUtils.getOurPubKeyStrFromCache();
const allContacts = await ContactsWrapperActions.getAll(); const allContactsInWrapper = await ContactsWrapperActions.getAll();
const contactsToRemoveFromDB = getContactsToRemoveFromDB(allContactsInWrapper);
await deleteContactsFromDB(contactsToRemoveFromDB);
for (let index = 0; index < allContacts.length; index++) { // create new contact conversation here, and update their state with what is part of the wrapper
const wrapperConvo = allContacts[index]; for (let index = 0; index < allContactsInWrapper.length; index++) {
const wrapperConvo = allContactsInWrapper[index];
if (wrapperConvo.id === us) { if (wrapperConvo.id === us) {
// our profile update comes from our userProfile, not from the contacts wrapper. // our profile update comes from our userProfile, not from the contacts wrapper.
@ -259,8 +325,10 @@ async function handleCommunitiesUpdate() {
for (let index = 0; index < communitiesToLeaveInDB.length; index++) { for (let index = 0; index < communitiesToLeaveInDB.length; index++) {
const toLeave = communitiesToLeaveInDB[index]; const toLeave = communitiesToLeaveInDB[index];
console.warn('leaving community with convoId ', toLeave.id); window.log.info('leaving community with convoId ', toLeave.id);
await getConversationController().deleteContact(toLeave.id, true); await getConversationController().deleteContact(toLeave.id, {
fromSyncMessage: true,
});
} }
// this call can take quite a long time and should not cause issues to not be awaited // this call can take quite a long time and should not cause issues to not be awaited
@ -328,7 +396,10 @@ async function handleLegacyGroupUpdate(latestEnvelopeTimestamp: number) {
for (let index = 0; index < legacyGroupsToLeaveInDB.length; index++) { for (let index = 0; index < legacyGroupsToLeaveInDB.length; index++) {
const toLeave = legacyGroupsToLeaveInDB[index]; const toLeave = legacyGroupsToLeaveInDB[index];
console.warn('leaving legacy group from configuration sync message with convoId ', toLeave.id); window.log.info(
'leaving legacy group from configuration sync message with convoId ',
toLeave.id
);
const toLeaveFromDb = getConversationController().get(toLeave.id); const toLeaveFromDb = getConversationController().get(toLeave.id);
// if we were kicked from that group, leave it as is until the user manually deletes it // if we were kicked from that group, leave it as is until the user manually deletes it
@ -336,13 +407,15 @@ async function handleLegacyGroupUpdate(latestEnvelopeTimestamp: number) {
if (!toLeaveFromDb?.get('isKickedFromGroup')) { if (!toLeaveFromDb?.get('isKickedFromGroup')) {
window.log.debug(`we were kicked from ${toLeave.id} so we keep it until manually deleted`); window.log.debug(`we were kicked from ${toLeave.id} so we keep it until manually deleted`);
await getConversationController().deleteContact(toLeave.id, true); await getConversationController().deleteContact(toLeave.id, {
fromSyncMessage: true,
});
} }
} }
for (let index = 0; index < legacyGroupsToJoinInDB.length; index++) { for (let index = 0; index < legacyGroupsToJoinInDB.length; index++) {
const toJoin = legacyGroupsToJoinInDB[index]; const toJoin = legacyGroupsToJoinInDB[index];
console.warn( window.log.info(
'joining legacy group from configuration sync message with convoId ', 'joining legacy group from configuration sync message with convoId ',
toJoin.pubkeyHex toJoin.pubkeyHex
); );

@ -82,7 +82,9 @@ async function joinOpenGroupV2(
// we already have a convo associated with it. Remove everything related to it so we start fresh // we already have a convo associated with it. Remove everything related to it so we start fresh
window?.log?.warn('leaving before rejoining open group v2 room', conversationId); window?.log?.warn('leaving before rejoining open group v2 room', conversationId);
await getConversationController().deleteContact(conversationId, true); await getConversationController().deleteContact(conversationId, {
fromSyncMessage: true,
});
} }
// Try to connect to server // Try to connect to server

@ -152,7 +152,9 @@ export class OpenGroupManagerV2 {
await OpenGroupData.removeV2OpenGroupRoom(roomConvoId); await OpenGroupData.removeV2OpenGroupRoom(roomConvoId);
getOpenGroupManager().removeRoomFromPolledRooms(infos); getOpenGroupManager().removeRoomFromPolledRooms(infos);
await getConversationController().deleteContact(roomConvoId, false); await getConversationController().deleteContact(roomConvoId, {
fromSyncMessage: false,
});
} }
} catch (e) { } catch (e) {
window?.log?.warn('cleanup roomInfos error', e); window?.log?.warn('cleanup roomInfos error', e);

@ -63,7 +63,7 @@ export async function doSnodeBatchRequest(
*/ */
function decodeBatchRequest(snodeResponse: SnodeResponse): NotEmptyArrayOfBatchResults { function decodeBatchRequest(snodeResponse: SnodeResponse): NotEmptyArrayOfBatchResults {
try { try {
// console.warn('decodeBatch: ', snodeResponse); // console.error('decodeBatch: ', snodeResponse);
if (snodeResponse.status !== 200) { if (snodeResponse.status !== 200) {
throw new Error(`decodeBatchRequest invalid status code: ${snodeResponse.status}`); throw new Error(`decodeBatchRequest invalid status code: ${snodeResponse.status}`);
} }

@ -24,7 +24,7 @@ import { getSwarmPollingInstance } from '../apis/snode_api';
import { SnodeNamespaces } from '../apis/snode_api/namespaces'; import { SnodeNamespaces } from '../apis/snode_api/namespaces';
import { ClosedGroupMemberLeftMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMemberLeftMessage'; import { ClosedGroupMemberLeftMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMemberLeftMessage';
import { UserUtils } from '../utils'; import { UserUtils } from '../utils';
import { isEmpty } from 'lodash'; import { isEmpty, isNil } from 'lodash';
let instance: ConversationController | null; let instance: ConversationController | null;
@ -200,9 +200,12 @@ export class ConversationController {
await conversation.commit(); await conversation.commit();
} }
public async deleteContact(id: string, fromSyncMessage: boolean) { public async deleteContact(
id: string,
options: { fromSyncMessage: boolean; justHidePrivate?: boolean }
) {
if (!this._initialFetchComplete) { if (!this._initialFetchComplete) {
throw new Error('getConversationController().deleteContact() needs complete initial fetch'); throw new Error('getConversationController.deleteContact needs complete initial fetch');
} }
window.log.info(`deleteContact with ${id}`); window.log.info(`deleteContact with ${id}`);
@ -227,18 +230,31 @@ export class ConversationController {
switch (convoType) { switch (convoType) {
case '1o1': case '1o1':
// if this conversation is a private conversation it's in fact a `contact` for desktop. // if this conversation is a private conversation it's in fact a `contact` for desktop.
// we just set the hidden field to true
// so the conversation still exists (needed for that user's profile in groups) but is not shown on the list of conversation. if (options.justHidePrivate || isNil(options.justHidePrivate) || conversation.isMe()) {
// We also keep the messages for now, as turning a contact as hidden might just be a temporary thing // we just set the hidden field to true
window.log.info(`deleteContact isPrivate, marking as hidden: ${id}`); // so the conversation still exists (needed for that user's profile in groups) but is not shown on the list of conversation.
conversation.set({ // We also keep the messages for now, as turning a contact as hidden might just be a temporary thing
priority: CONVERSATION_PRIORITIES.hidden, window.log.info(`deleteContact isPrivate, marking as hidden: ${id}`);
}); conversation.set({
// we currently do not wish to reset the approved/approvedMe state when marking a private conversation as hidden priority: CONVERSATION_PRIORITIES.hidden,
// await conversation.setIsApproved(false, false); });
await conversation.commit(); // this updates the wrappers content to reflect the hidden state // We don't remove entries from the contacts wrapper, so better keep corresponding convo volatile info for now (it will be pruned if needed)
await conversation.commit(); // this updates the wrappers content to reflect the hidden state
// We don't remove entries from the contacts wrapper, so better keep corresponding convo volatile info for now (it will be pruned if needed) } else {
window.log.info(`deleteContact isPrivate, reset fields and removing from wrapper: ${id}`);
await conversation.setIsApproved(false, false);
await conversation.setDidApproveMe(false, false);
conversation.set('active_at', 0);
await BlockedNumberController.unblockAll([conversation.id]);
await conversation.commit(); // first commit to DB so the DB knows about the changes
if (SessionUtilContact.isContactToStoreInWrapper(conversation)) {
window.log.warn('isContactToStoreInWrapper still true for ', conversation.attributes);
}
await SessionUtilContact.removeContactFromWrapper(conversation.id); // then remove the entry alltogether from the wrapper
}
break; break;
case 'Community': case 'Community':
window?.log?.info('leaving open group v2', conversation.id); window?.log?.info('leaving open group v2', conversation.id);
@ -280,14 +296,14 @@ export class ConversationController {
break; break;
case 'LegacyGroup': case 'LegacyGroup':
window.log.info(`deleteContact ClosedGroup case: ${conversation.id}`); window.log.info(`deleteContact ClosedGroup case: ${conversation.id}`);
await leaveClosedGroup(conversation.id, fromSyncMessage); // this removes the data from the group and convo volatile info await leaveClosedGroup(conversation.id, options.fromSyncMessage); // this removes the data from the group and convo volatile info
await this.cleanUpGroupConversation(conversation.id); await this.cleanUpGroupConversation(conversation.id);
break; break;
default: default:
assertUnreachable(convoType, `deleteContact: convoType ${convoType} not handled`); assertUnreachable(convoType, `deleteContact: convoType ${convoType} not handled`);
} }
if (!fromSyncMessage) { if (!options.fromSyncMessage) {
await ConfigurationSync.queueNewJobIfNeeded(); await ConfigurationSync.queueNewJobIfNeeded();
} }
} }

@ -68,7 +68,7 @@ export async function generateGroupV3Keypair() {
preprendedPubkey.set(publicKey, 1); preprendedPubkey.set(publicKey, 1);
preprendedPubkey[0] = 3; preprendedPubkey[0] = 3;
console.warn(`generateGroupV3Keypair: pubkey${toHex(preprendedPubkey)}`); // console.warn(`generateGroupV3Keypair: pubkey${toHex(preprendedPubkey)}`);
return { pubkey: toHex(preprendedPubkey), privateKey: toHex(ed25519KeyPair.privateKey) }; return { pubkey: toHex(preprendedPubkey), privateKey: toHex(ed25519KeyPair.privateKey) };
} }

@ -29,7 +29,7 @@ export class ExpirationTimerUpdateMessage extends DataMessage {
data.flags = SignalService.DataMessage.Flags.EXPIRATION_TIMER_UPDATE; data.flags = SignalService.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
// TODO we shouldn't need this once android recieving refactor is done. // TODOLATER we won't need this once legacy groups are not supported anymore
// the envelope stores the groupId for a closed group already. // the envelope stores the groupId for a closed group already.
if (this.groupId) { if (this.groupId) {
const groupMessage = new SignalService.GroupContext(); const groupMessage = new SignalService.GroupContext();

@ -331,7 +331,7 @@ export class MessageQueue {
// or a message with a syncTarget set. // or a message with a syncTarget set.
if (MessageSender.isSyncMessage(message)) { if (MessageSender.isSyncMessage(message)) {
window?.log?.warn('OutgoingMessageQueue: Processing sync message'); window?.log?.info('OutgoingMessageQueue: Processing sync message');
isSyncMessage = true; isSyncMessage = true;
} else { } else {
window?.log?.warn('Dropping message in process() to be sent to ourself'); window?.log?.warn('Dropping message in process() to be sent to ourself');

@ -287,11 +287,12 @@ async function queueNewJobIfNeeded() {
!lastRunConfigSyncJobTimestamp || !lastRunConfigSyncJobTimestamp ||
lastRunConfigSyncJobTimestamp < Date.now() - defaultMsBetweenRetries lastRunConfigSyncJobTimestamp < Date.now() - defaultMsBetweenRetries
) { ) {
window.log.debug('Scheduling ConfSyncJob: ASAP');
// we postpone by 1000ms to make sure whoever is adding this job is done with what is needs to do first
// this call will make sure that there is only one configuration sync job at all times // this call will make sure that there is only one configuration sync job at all times
await runners.configurationSyncRunner.addJob( await runners.configurationSyncRunner.addJob(
new ConfigurationSyncJob({ nextAttemptTimestamp: Date.now() + 1000 }) new ConfigurationSyncJob({ nextAttemptTimestamp: Date.now() + 1000 })
); );
window.log.debug('Scheduling ConfSyncJob: ASAP'); // we postpone by 1000ms to make sure whoever is adding this job is done with what is needs to do first
} else { } else {
// if we did run at t=100, and it is currently t=110, the difference is 10 // if we did run at t=100, and it is currently t=110, the difference is 10
const diff = Math.max(Date.now() - lastRunConfigSyncJobTimestamp, 0); const diff = Math.max(Date.now() - lastRunConfigSyncJobTimestamp, 0);

@ -139,7 +139,7 @@ async function pendingChangesForPubkey(pubkey: string): Promise<Array<OutgoingCo
namespace, namespace,
}); });
} }
window.log.debug(`those variants needs push: "${[...variantsNeedingPush]}"`); window.log.info(`those variants needs push: "${[...variantsNeedingPush]}"`);
return results; return results;
} }

@ -33,7 +33,6 @@ function isContactToStoreInWrapper(convo: ConversationModel): boolean {
!convo.isMe() && convo.isPrivate() && convo.isActive() && !PubKey.hasBlindedPrefix(convo.id) !convo.isMe() && convo.isPrivate() && convo.isActive() && !PubKey.hasBlindedPrefix(convo.id)
); );
} }
// TODOLATER should we allow a blinded pubkey to be in the contact wrapper when we blocked it (can we block a blinded message request?)
/** /**
* Fetches the specified convo and updates the required field in the wrapper. * Fetches the specified convo and updates the required field in the wrapper.
@ -47,9 +46,6 @@ async function insertContactFromDBIntoWrapperAndRefresh(id: string): Promise<voi
} }
if (!isContactToStoreInWrapper(foundConvo)) { if (!isContactToStoreInWrapper(foundConvo)) {
console.warn(
`insertContactFromDBIntoWrapperAndRefresh: convo ${id} should not be saved. Skipping`
);
return; return;
} }
@ -124,6 +120,7 @@ async function removeContactFromWrapper(id: string) {
} catch (e) { } catch (e) {
window.log.warn(`ContactsWrapperActions.erase of ${id} failed with ${e.message}`); window.log.warn(`ContactsWrapperActions.erase of ${id} failed with ${e.message}`);
} }
await refreshMappedValue(id);
} }
export const SessionUtilContact = { export const SessionUtilContact = {
isContactToStoreInWrapper, isContactToStoreInWrapper,

@ -1055,3 +1055,7 @@ export const getIsSelectedConvoInitialLoadingInProgress = createSelector(
getSelectedConversation, getSelectedConversation,
(convo: ReduxConversationType | undefined): boolean => Boolean(convo?.isInitialFetchingInProgress) (convo: ReduxConversationType | undefined): boolean => Boolean(convo?.isInitialFetchingInProgress)
); );
export function getCurrentlySelectedConversationOutsideRedux() {
return window?.inboxStore?.getState().conversations.selectedConversation as string | undefined;
}

@ -11,6 +11,7 @@ import {
waitForTestIdWithText, waitForTestIdWithText,
} from './utils'; } from './utils';
let window: Page | undefined; let window: Page | undefined;
// tslint:disable: no-console
test.beforeEach(beforeAllClean); test.beforeEach(beforeAllClean);
@ -61,7 +62,7 @@ test.describe('Password checks', () => {
// Change password // Change password
await clickOnTestIdWithText(window, 'change-password-settings-button', 'Change Password'); await clickOnTestIdWithText(window, 'change-password-settings-button', 'Change Password');
console.warn('clicked Change Password'); console.info('clicked Change Password');
// Enter old password // Enter old password
await typeIntoInput(window, 'password-input', testPassword); await typeIntoInput(window, 'password-input', testPassword);
// Enter new password // Enter new password

@ -106,7 +106,7 @@ describe('ReactionMessage', () => {
expect(reaction, 'no reaction should be returned since we are over the rate limit').to.be expect(reaction, 'no reaction should be returned since we are over the rate limit').to.be
.undefined; .undefined;
clock = useFakeTimers(Date.now()); clock = useFakeTimers({ now: Date.now(), shouldAdvanceTime: true });
// Wait a miniute for the rate limit to clear // Wait a miniute for the rate limit to clear
clock.tick(1 * 60 * 1000); clock.tick(1 * 60 * 1000);

@ -80,13 +80,10 @@ describe('MessageSender', () => {
}); });
it('should only retry the specified amount of times before throwing', async () => { it('should only retry the specified amount of times before throwing', async () => {
// const clock = sinon.useFakeTimers();
sessionMessageAPISendStub.throws(new Error('API error')); sessionMessageAPISendStub.throws(new Error('API error'));
const attempts = 2; const attempts = 2;
const promise = MessageSender.send(rawMessage, attempts, 10); const promise = MessageSender.send(rawMessage, attempts, 10);
await expect(promise).is.rejectedWith('API error'); await expect(promise).is.rejectedWith('API error');
// clock.restore();
expect(sessionMessageAPISendStub.callCount).to.equal(attempts); expect(sessionMessageAPISendStub.callCount).to.equal(attempts);
}); });

@ -64,7 +64,7 @@ describe('SwarmPolling', () => {
swarmPolling.resetSwarmPolling(); swarmPolling.resetSwarmPolling();
pollOnceForKeySpy = Sinon.spy(swarmPolling, 'pollOnceForKey'); pollOnceForKeySpy = Sinon.spy(swarmPolling, 'pollOnceForKey');
clock = sinon.useFakeTimers(Date.now()); clock = sinon.useFakeTimers({ now: Date.now(), shouldAdvanceTime: true });
}); });
afterEach(() => { afterEach(() => {
@ -321,14 +321,16 @@ describe('SwarmPolling', () => {
const groupConvoPubkey = PubKey.cast(convo.id as string); const groupConvoPubkey = PubKey.cast(convo.id as string);
swarmPolling.addGroupId(groupConvoPubkey); swarmPolling.addGroupId(groupConvoPubkey);
await swarmPolling.start(true); await swarmPolling.start(true);
expect(pollOnceForKeySpy.callCount).to.eq(2);
expect(pollOnceForKeySpy.firstCall.args).to.deep.eq([ourPubkey, false, [0]]);
expect(pollOnceForKeySpy.secondCall.args).to.deep.eq([groupConvoPubkey, true, [-10]]);
clock.tick(9000); clock.tick(9000);
// no need to do that as the tick will trigger a call in all cases after 5 secs await swarmPolling.pollForAllKeys(); // no need to do that as the tick will trigger a call in all cases after 5 secs await swarmPolling.pollForAllKeys();
/** this is not easy to explain, but /** this is not easy to explain, but
* - during the swarmPolling.start, we get two calls to pollOnceForKeySpy (one for our id and one for group od) * - during the swarmPolling.start, we get two calls to pollOnceForKeySpy (one for our id and one for group id)
* - the clock ticks 9sec, and another call of pollOnceForKeySpy get started, but as we do not await them, this test fails. * - the clock ticks 9sec, and another call of pollOnceForKeySpy get started, but as we do not await them, this test fails.
* the only fix is to restore the clock and force the a small sleep to let the thing run in bg * the only fix is to restore the clock and force the a small sleep to let the thing run in bg
*/ */
clock.restore();
await sleepFor(10); await sleepFor(10);
expect(pollOnceForKeySpy.callCount).to.eq(4); expect(pollOnceForKeySpy.callCount).to.eq(4);
@ -360,7 +362,6 @@ describe('SwarmPolling', () => {
* - the clock ticks 9sec, and another call of pollOnceForKeySpy get started, but as we do not await them, this test fails. * - the clock ticks 9sec, and another call of pollOnceForKeySpy get started, but as we do not await them, this test fails.
* the only fix is to restore the clock and force the a small sleep to let the thing run in bg * the only fix is to restore the clock and force the a small sleep to let the thing run in bg
*/ */
clock.restore();
await sleepFor(10); await sleepFor(10);
// we should have two more calls here, so 4 total. // we should have two more calls here, so 4 total.
expect(pollOnceForKeySpy.callCount).to.eq(4); expect(pollOnceForKeySpy.callCount).to.eq(4);
@ -386,6 +387,7 @@ describe('SwarmPolling', () => {
convo.set('active_at', Date.now() - 7 * 24 * 3600 * 1000 - 3600 * 1000); convo.set('active_at', Date.now() - 7 * 24 * 3600 * 1000 - 3600 * 1000);
clock.tick(1 * 60 * 1000); clock.tick(1 * 60 * 1000);
await sleepFor(10);
// we should have only one more call here, the one for our direct pubkey fetch // we should have only one more call here, the one for our direct pubkey fetch
expect(pollOnceForKeySpy.callCount).to.eq(3); expect(pollOnceForKeySpy.callCount).to.eq(3);

@ -11,6 +11,7 @@ import {
import { sleepFor } from '../../../../../session/utils/Promise'; import { sleepFor } from '../../../../../session/utils/Promise';
import { stubData } from '../../../../test-utils/utils'; import { stubData } from '../../../../test-utils/utils';
import { TestUtils } from '../../../../test-utils'; import { TestUtils } from '../../../../test-utils';
// tslint:disable: no-console
function getFakeSleepForJob(timestamp: number): FakeSleepForJob { function getFakeSleepForJob(timestamp: number): FakeSleepForJob {
const job = new FakeSleepForJob({ const job = new FakeSleepForJob({
@ -200,12 +201,12 @@ describe('JobRunner', () => {
expect(runnerMulti.getJobList()).to.deep.eq([job.serializeJob(), job2.serializeJob()]); expect(runnerMulti.getJobList()).to.deep.eq([job.serializeJob(), job2.serializeJob()]);
expect(runnerMulti.getCurrentJobIdentifier()).to.be.equal(job.persistedData.identifier); expect(runnerMulti.getCurrentJobIdentifier()).to.be.equal(job.persistedData.identifier);
console.warn( console.info(
'runnerMulti.getJobList() initial', 'runnerMulti.getJobList() initial',
runnerMulti.getJobList().map(m => m.identifier), runnerMulti.getJobList().map(m => m.identifier),
Date.now() Date.now()
); );
console.warn('=========== awaiting first job =========='); console.info('=========== awaiting first job ==========');
// each job takes 5s to finish, so let's tick once the first one should be done // each job takes 5s to finish, so let's tick once the first one should be done
clock.tick(5000); clock.tick(5000);
@ -214,10 +215,10 @@ describe('JobRunner', () => {
expect(awaited).to.eq('await'); expect(awaited).to.eq('await');
await sleepFor(10); await sleepFor(10);
console.warn('=========== awaited first job =========='); console.info('=========== awaited first job ==========');
expect(runnerMulti.getCurrentJobIdentifier()).to.be.equal(job2.persistedData.identifier); expect(runnerMulti.getCurrentJobIdentifier()).to.be.equal(job2.persistedData.identifier);
console.warn('=========== awaiting second job =========='); console.info('=========== awaiting second job ==========');
clock.tick(5000); clock.tick(5000);
@ -225,7 +226,7 @@ describe('JobRunner', () => {
expect(awaited).to.eq('await'); expect(awaited).to.eq('await');
await sleepFor(10); // those sleep for is just to let the runner the time to finish writing the tests to the DB and exit the handling of the previous test await sleepFor(10); // those sleep for is just to let the runner the time to finish writing the tests to the DB and exit the handling of the previous test
console.warn('=========== awaited second job =========='); console.info('=========== awaited second job ==========');
expect(runnerMulti.getCurrentJobIdentifier()).to.eq(null); expect(runnerMulti.getCurrentJobIdentifier()).to.eq(null);
@ -245,27 +246,27 @@ describe('JobRunner', () => {
expect(runnerMulti.getCurrentJobIdentifier()).to.be.equal(job.persistedData.identifier); expect(runnerMulti.getCurrentJobIdentifier()).to.be.equal(job.persistedData.identifier);
clock.tick(5000); clock.tick(5000);
console.warn('=========== awaiting first job =========='); console.info('=========== awaiting first job ==========');
await runnerMulti.waitCurrentJob(); await runnerMulti.waitCurrentJob();
// just give some time for the runnerMulti to pick up a new job // just give some time for the runnerMulti to pick up a new job
await sleepFor(10); await sleepFor(10);
expect(runnerMulti.getJobList()).to.deep.eq([]); expect(runnerMulti.getJobList()).to.deep.eq([]);
expect(runnerMulti.getCurrentJobIdentifier()).to.be.equal(null); expect(runnerMulti.getCurrentJobIdentifier()).to.be.equal(null);
console.warn('=========== awaited first job =========='); console.info('=========== awaited first job ==========');
// the first job should already be finished now // the first job should already be finished now
result = await runnerMulti.addJob(job2); result = await runnerMulti.addJob(job2);
expect(result).to.eq('job_started'); expect(result).to.eq('job_started');
expect(runnerMulti.getJobList()).to.deep.eq([job2.serializeJob()]); expect(runnerMulti.getJobList()).to.deep.eq([job2.serializeJob()]);
console.warn('=========== awaiting second job =========='); console.info('=========== awaiting second job ==========');
// each job takes 5s to finish, so let's tick once the first one should be done // each job takes 5s to finish, so let's tick once the first one should be done
clock.tick(5010); clock.tick(5010);
await runnerMulti.waitCurrentJob(); await runnerMulti.waitCurrentJob();
await sleepFor(10); await sleepFor(10);
console.warn('=========== awaited second job =========='); console.info('=========== awaited second job ==========');
expect(runnerMulti.getJobList()).to.deep.eq([]); expect(runnerMulti.getJobList()).to.deep.eq([]);
}); });

@ -100,6 +100,7 @@ export type LocalizerKeys =
| 'deleteMessagesQuestion' | 'deleteMessagesQuestion'
| 'deleteMessageQuestion' | 'deleteMessageQuestion'
| 'deleteMessages' | 'deleteMessages'
| 'deleteConversation'
| 'deleted' | 'deleted'
| 'messageDeletedPlaceholder' | 'messageDeletedPlaceholder'
| 'from' | 'from'

Loading…
Cancel
Save