fix: delete messages for groups by admin are marked as deleted

pull/3052/head
Audric Ackermann 7 months ago
parent 4c4806df47
commit db22094898
No known key found for this signature in database

@ -40,7 +40,7 @@ window.saveLog = additionalText => ipc.send('save-debug-log', additionalText);
window.sessionFeatureFlags = {
useOnionRequests: true,
useTestNet: isTestNet() || isTestIntegration(),
useClosedGroupV2: false, // TODO DO NOT MERGE Remove after QA
useClosedGroupV2: true, // TODO DO NOT MERGE Remove after QA
useClosedGroupV2QAButtons: true, // TODO DO NOT MERGE Remove after QA
replaceLocalizedStringsWithKeys: false,
debug: {
@ -261,7 +261,6 @@ data.initData();
const { ConvoHub } = require('./ts/session/conversations/ConversationController');
window.getConversationController = ConvoHub.use;
// Linux seems to periodically let the event loop stop, so this is a global workaround
setInterval(() => {
window.nodeSetImmediate(() => {});

@ -25,6 +25,7 @@ import {
useSelectedNicknameOrProfileNameOrShortenedPubkey,
} from '../../state/selectors/selectedConversation';
import {
useLibGroupDestroyed,
useLibGroupInviteGroupName,
useLibGroupInvitePending,
useLibGroupKicked,
@ -112,10 +113,10 @@ export const ConversationIncomingRequestExplanation = () => {
const showMsgRequestUI = selectedConversation && isIncomingMessageRequest;
const hasOutgoingMessages = useSelector(hasSelectedConversationOutgoingMessages);
const isGroupV2 = useSelectedIsGroupV2()
const isGroupV2 = useSelectedIsGroupV2();
if (isGroupV2) {
return <GroupRequestExplanation />
return <GroupRequestExplanation />;
}
if (!showMsgRequestUI || hasOutgoingMessages) {
@ -212,6 +213,7 @@ export const NoMessageInConversation = () => {
const isPrivate = useSelectedIsPrivate();
const isIncomingRequest = useIsIncomingRequest(selectedConversation);
const isKickedFromGroup = useLibGroupKicked(selectedConversation);
const isGroupDestroyed = useLibGroupDestroyed(selectedConversation);
const name = useSelectedNicknameOrProfileNameOrShortenedPubkey();
const getHtmlToRender = () => {
@ -227,6 +229,10 @@ export const NoMessageInConversation = () => {
return localize('messageRequestsTurnedOff').withArgs({ name }).toString();
}
if (isGroupV2 && isGroupDestroyed) {
return localize('groupDeletedMemberDescription').withArgs({ group_name: name }).toString();
}
if (isGroupV2 && isKickedFromGroup) {
return localize('groupRemovedYou').withArgs({ group_name: name }).toString();
}

@ -3,6 +3,7 @@ import { Mention, MentionsInput } from 'react-mentions';
import {
useSelectedConversationKey,
useSelectedIsBlocked,
useSelectedIsGroupDestroyed,
useSelectedIsKickedFromGroup,
useSelectedNicknameOrProfileNameOrShortenedPubkey,
} from '../../../state/selectors/selectedConversation';
@ -55,6 +56,7 @@ export const CompositionTextArea = (props: Props) => {
const selectedConversationKey = useSelectedConversationKey();
const htmlDirection = useHTMLDirection();
const isKickedFromGroup = useSelectedIsKickedFromGroup();
const isGroupDestroyed = useSelectedIsGroupDestroyed();
const isBlocked = useSelectedIsBlocked();
const groupName = useSelectedNicknameOrProfileNameOrShortenedPubkey();
@ -63,6 +65,9 @@ export const CompositionTextArea = (props: Props) => {
}
const makeMessagePlaceHolderText = () => {
if (isGroupDestroyed) {
return window.i18n('groupDeletedMemberDescription', { group_name: groupName });
}
if (isKickedFromGroup) {
return window.i18n('groupRemovedYou', { group_name: groupName });
}

@ -29,7 +29,7 @@ export const MessageText = (props: Props) => {
}
const { text, isDeleted, conversationType } = selected;
const contents = isDeleted ? window.i18n('deleteMessageDeleted', { count: 1 }) : text?.trim();
const contents = isDeleted ? window.i18n('deleteMessageDeletedGlobally') : text?.trim();
if (!contents) {
return null;

@ -21,18 +21,17 @@ import {
showUpdateGroupNameByConvoId,
} from '../../../../interactions/conversationInteractions';
import { Constants } from '../../../../session';
import { ConvoHub } from '../../../../session/conversations';
import { PubKey } from '../../../../session/types';
import { hasClosedGroupV2QAButtons } from '../../../../shared/env_vars';
import { closeRightPanel } from '../../../../state/ducks/conversations';
import { groupInfoActions } from '../../../../state/ducks/metaGroups';
import { updateConfirmModal } from '../../../../state/ducks/modalDialog';
import { resetRightOverlayMode, setRightOverlayMode } from '../../../../state/ducks/section';
import {
useSelectedConversationKey,
useSelectedDisplayNameInProfile,
useSelectedIsActive,
useSelectedIsBlocked,
useSelectedIsGroupDestroyed,
useSelectedIsGroupOrCommunity,
useSelectedIsGroupV2,
useSelectedIsKickedFromGroup,
@ -45,7 +44,6 @@ import { AttachmentTypeWithPath } from '../../../../types/Attachment';
import { getAbsoluteAttachmentPath } from '../../../../types/MessageAttachment';
import { Avatar, AvatarSize } from '../../../avatar/Avatar';
import { Flex } from '../../../basic/Flex';
import { SessionButtonColor } from '../../../basic/SessionButton';
import { SpacerLG, SpacerMD, SpacerXL } from '../../../basic/Text';
import { PanelButtonGroup, PanelIconButton } from '../../../buttons';
import { MediaItemType } from '../../../lightbox/LightboxGallery';
@ -134,6 +132,7 @@ const HeaderItem = () => {
const dispatch = useDispatch();
const isBlocked = useSelectedIsBlocked();
const isKickedFromGroup = useSelectedIsKickedFromGroup();
const isGroupDestroyed = useSelectedIsGroupDestroyed();
const isGroup = useSelectedIsGroupOrCommunity();
const isGroupV2 = useSelectedIsGroupV2();
const isPublic = useSelectedIsPublic();
@ -146,7 +145,8 @@ const HeaderItem = () => {
const showInviteLegacyGroup =
!isPublic && !isGroupV2 && isGroup && !isKickedFromGroup && !isBlocked;
const showInviteGroupV2 = isGroupV2 && !isKickedFromGroup && !isBlocked && weAreAdmin;
const showInviteGroupV2 =
isGroupV2 && !isKickedFromGroup && !isBlocked && weAreAdmin && !isGroupDestroyed;
const showInviteContacts = isPublic || showInviteLegacyGroup || showInviteGroupV2;
const showMemberCount = !!(subscriberCount && subscriberCount > 0);
@ -199,43 +199,6 @@ const StyledName = styled.h4`
font-size: var(--font-size-md);
`;
const DestroyGroupForAllMembersButton = () => {
const dispatch = useDispatch();
const groupPk = useSelectedConversationKey();
if (groupPk && PubKey.is03Pubkey(groupPk) && hasClosedGroupV2QAButtons()) {
return (
<PanelIconButton
dataTestId="delete-group-button"
iconType="delete"
color={'var(--danger-color)'}
text={window.i18n('groupDelete')}
onClick={() => {
dispatch(
// TODO build the right UI for this (just adding buttons for QA for now)
updateConfirmModal({
okText: window.i18n('delete'),
okTheme: SessionButtonColor.Danger,
title: window.i18n('groupDelete'),
conversationId: groupPk,
onClickOk: () => {
void ConvoHub.use().deleteGroup(groupPk, {
deleteAllMessagesOnSwarm: true,
deletionType: 'doNotKeep',
fromSyncMessage: false,
sendLeaveMessage: false,
forceDestroyForAllMembers: true,
});
},
})
);
}}
/>
);
}
return null;
};
export const OverlayRightPanelSettings = () => {
const [documents, setDocuments] = useState<Array<MediaItemType>>([]);
const [media, setMedia] = useState<Array<MediaItemType>>([]);
@ -447,7 +410,6 @@ export const OverlayRightPanelSettings = () => {
color={'var(--danger-color)'}
iconType={'delete'}
/>
<DestroyGroupForAllMembersButton />
</>
)}
</PanelButtonGroup>

@ -62,6 +62,7 @@ import {
ConversationInteractionStatus,
ConversationInteractionType,
} from '../../interactions/types';
import { useLibGroupDestroyed } from '../../state/selectors/userGroups';
/** Menu items standardized */
@ -213,9 +214,10 @@ export const ShowUserDetailsMenuItem = () => {
export const UpdateGroupNameMenuItem = () => {
const convoId = useConvoIdFromContext();
const isKickedFromGroup = useIsKickedFromGroup(convoId);
const isDestroyed = useLibGroupDestroyed(convoId);
const weAreAdmin = useWeAreAdmin(convoId);
if (!isKickedFromGroup && weAreAdmin) {
if (!isKickedFromGroup && weAreAdmin && !isDestroyed) {
return (
<ItemWithDataTestId
onClick={() => {
@ -232,6 +234,7 @@ export const UpdateGroupNameMenuItem = () => {
export const RemoveModeratorsMenuItem = (): JSX.Element | null => {
const convoId = useConvoIdFromContext();
const isPublic = useIsPublic(convoId);
const isKickedFromGroup = useIsKickedFromGroup(convoId);
const weAreAdmin = useWeAreAdmin(convoId);
@ -517,6 +520,8 @@ export const NotificationForConvoMenuItem = (): JSX.Element | null => {
const isBlocked = useIsBlocked(convoId);
const isActive = useIsActive(convoId);
const isKickedFromGroup = useIsKickedFromGroup(convoId);
const isGroupDestroyed = useLibGroupDestroyed(convoId);
const isFriend = useIsPrivateAndFriend(convoId);
const isPrivate = useIsPrivate(convoId);
const isMessageRequestShown = useSelector(getIsMessageRequestOverlayShown);
@ -525,6 +530,7 @@ export const NotificationForConvoMenuItem = (): JSX.Element | null => {
!convoId ||
isMessageRequestShown ||
isKickedFromGroup ||
isGroupDestroyed ||
isBlocked ||
!isActive ||
(isPrivate && !isFriend)

@ -27,10 +27,9 @@ import * as dataInit from './dataInit';
import { cleanData } from './dataUtils';
import { SNODE_POOL_ITEM_ID } from './settings-key';
import {
DataCallArgs,
DeleteAllMessageFromSendersInConversationType,
DeleteAllMessageHashesInConversationMatchingAuthorType,
DeleteAllMessageHashesInConversationType,
FindAllMessageFromSendersInConversationTypeArgs,
FindAllMessageHashesInConversationMatchingAuthorTypeArgs,
FindAllMessageHashesInConversationTypeArgs,
} from './sharedDataTypes';
import { GuardNode, Snode } from './types';
@ -574,22 +573,40 @@ async function removeAllMessagesInConversation(conversationId: string): Promise<
);
}
async function deleteAllMessageFromSendersInConversation(
args: DataCallArgs<DeleteAllMessageFromSendersInConversationType>
): ReturnType<DeleteAllMessageFromSendersInConversationType> {
return channels.deleteAllMessageFromSendersInConversation(args);
async function findAllMessageFromSendersInConversation(
args: FindAllMessageFromSendersInConversationTypeArgs
): Promise<Array<MessageModel>> {
const msgAttrs = await channels.findAllMessageFromSendersInConversation(args);
if (!msgAttrs || isEmpty(msgAttrs)) {
return [];
}
return msgAttrs.map((msg: any) => new MessageModel(msg));
}
async function deleteAllMessageHashesInConversation(
args: DataCallArgs<DeleteAllMessageHashesInConversationType>
): ReturnType<DeleteAllMessageHashesInConversationType> {
return channels.deleteAllMessageHashesInConversation(args);
async function findAllMessageHashesInConversation(
args: FindAllMessageHashesInConversationTypeArgs
): Promise<Array<MessageModel>> {
const msgAttrs = await channels.findAllMessageHashesInConversation(args);
if (!msgAttrs || isEmpty(msgAttrs)) {
return [];
}
return msgAttrs.map((msg: any) => new MessageModel(msg));
}
async function deleteAllMessageHashesInConversationMatchingAuthor(
args: DataCallArgs<DeleteAllMessageHashesInConversationMatchingAuthorType>
): ReturnType<DeleteAllMessageHashesInConversationMatchingAuthorType> {
return channels.deleteAllMessageHashesInConversationMatchingAuthor(args);
async function findAllMessageHashesInConversationMatchingAuthor(
args: FindAllMessageHashesInConversationMatchingAuthorTypeArgs
): Promise<Array<MessageModel>> {
const msgAttrs = await channels.findAllMessageHashesInConversationMatchingAuthor(args);
if (!msgAttrs || isEmpty(msgAttrs)) {
return [];
}
return msgAttrs.map((msg: any) => new MessageModel(msg));
}
async function getMessagesBySentAt(sentAt: number): Promise<MessageCollection> {
@ -873,9 +890,9 @@ export const Data = {
getLastHashBySnode,
getSeenMessagesByHashList,
removeAllMessagesInConversation,
deleteAllMessageFromSendersInConversation,
deleteAllMessageHashesInConversation,
deleteAllMessageHashesInConversationMatchingAuthor,
findAllMessageFromSendersInConversation,
findAllMessageHashesInConversation,
findAllMessageHashesInConversationMatchingAuthor,
getMessagesBySentAt,
getExpiredMessages,
getOutgoingWithoutExpiresAt,

@ -51,9 +51,9 @@ const channelsToMake = new Set([
'getUnreadCountByConversation',
'getMessageCountByType',
'removeAllMessagesInConversation',
'deleteAllMessageFromSendersInConversation',
'deleteAllMessageHashesInConversation',
'deleteAllMessageHashesInConversationMatchingAuthor',
'findAllMessageFromSendersInConversation',
'findAllMessageHashesInConversation',
'findAllMessageHashesInConversationMatchingAuthor',
'getMessageCount',
'filterAlreadyFetchedOpengroupMessage',
'getMessagesBySenderAndSentAt',

@ -1,25 +1,18 @@
import { PubkeyType, WithGroupPubkey } from 'libsession_util_nodejs';
export type DataCallArgs<T extends (args: any) => any> = Parameters<T>[0];
export type DeleteAllMessageFromSendersInConversationType = (
args: WithGroupPubkey & {
export type FindAllMessageFromSendersInConversationTypeArgs = WithGroupPubkey & {
toRemove: Array<PubkeyType>;
signatureTimestamp: number;
}
) => Promise<{ messageHashes: Array<string> }>;
};
export type DeleteAllMessageHashesInConversationType = (
args: WithGroupPubkey & {
messageHashes: Array<string>;
signatureTimestamp: number;
}
) => Promise<{ messageHashes: Array<string> }>;
export type FindAllMessageHashesInConversationTypeArgs = WithGroupPubkey & {
messageHashes: Array<string>;
signatureTimestamp: number;
};
export type DeleteAllMessageHashesInConversationMatchingAuthorType = (
args: WithGroupPubkey & {
messageHashes: Array<string>;
author: PubkeyType;
signatureTimestamp: number;
}
) => Promise<{ msgIdsDeleted: Array<string>; msgHashesDeleted: Array<string> }>;
export type FindAllMessageHashesInConversationMatchingAuthorTypeArgs = WithGroupPubkey & {
messageHashes: Array<string>;
author: PubkeyType;
signatureTimestamp: number;
};

@ -21,7 +21,11 @@ import {
import { useLibGroupAdmins, useLibGroupMembers, useLibGroupName } from '../state/selectors/groups';
import { isPrivateAndFriend } from '../state/selectors/selectedConversation';
import { useOurPkStr } from '../state/selectors/user';
import { useLibGroupInvitePending, useLibGroupKicked } from '../state/selectors/userGroups';
import {
useLibGroupDestroyed,
useLibGroupInvitePending,
useLibGroupKicked,
} from '../state/selectors/userGroups';
export function useAvatarPath(convoId: string | undefined) {
const convoProps = useConversationPropsById(convoId);
@ -217,6 +221,14 @@ export function useIsKickedFromGroup(convoId?: string) {
return Boolean(convoProps && (convoProps.isKickedFromGroup || libIsKicked)); // not ideal, but until we trust what we get from libsession for all cases, we have to either trust what we have in the DB
}
export function useIsGroupDestroyed(convoId?: string) {
const libIsDestroyed = useLibGroupDestroyed(convoId);
if (convoId && PubKey.is03Pubkey(convoId)) {
return libIsDestroyed;
}
return false;
}
export function useWeAreAdmin(convoId?: string) {
const groupAdmins = useGroupAdmins(convoId);
const us = useOurPkStr();

@ -445,7 +445,7 @@ export async function showLeaveGroupByConvoId(conversationId: string, name: stri
const isAdmin = admins.includes(UserUtils.getOurPubKeyStrFromCache());
const showOnlyGroupAdminWarning = isClosedGroup && isAdmin;
const weAreLastAdmin =
PubKey.is05Pubkey(conversationId) ||
(PubKey.is05Pubkey(conversationId) && isAdmin && admins.length === 1) ||
(PubKey.is03Pubkey(conversationId) && isAdmin && admins.length === 1);
const lastMessageInteractionType = conversation.get('lastMessageInteractionType');
const lastMessageInteractionStatus = conversation.get('lastMessageInteractionStatus');

@ -280,17 +280,6 @@ export async function deleteMessagesFromSwarmAndMarkAsDeletedLocally(
);
return;
}
if (conversation.isClosedGroupV2() && PubKey.is03Pubkey(conversation.id)) {
window.log.info(
'Cannot delete messages from a legacy closed group swarm, so we just markDeleted.'
);
await Promise.all(
messages.map(async message => {
return deleteMessageLocallyOnly({ conversation, message, deletionType: 'markDeleted' });
})
);
return;
}
// we can only delete messages on the swarm when they are on our own swarm, or it is a groupv2 that we are the admin off
const pubkeyToDeleteFrom = PubKey.is03Pubkey(conversation.id)

@ -92,3 +92,4 @@ export const crowdinLocales = [
] as const;
export type CrowdinLocale = (typeof crowdinLocales)[number];

@ -438,7 +438,6 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
);
}
return window.i18n.stripped(...([i18nProps.token] as GetMessageArgs<LocalizerToken>));
}
const body = this.get('body');
if (body) {
@ -986,7 +985,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
public async markAsDeleted() {
this.set({
isDeleted: true,
body: window.i18n('deleteMessageDeleted', { count: 1 }),
body: window.i18n('deleteMessageDeletedGlobally'),
quote: undefined,
groupInvitation: undefined,
dataExtractionNotification: undefined,
@ -997,6 +996,11 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
preview: undefined,
reacts: undefined,
reactsIndex: undefined,
flags: undefined,
callNotificationType: undefined,
interactionNotification: undefined,
reaction: undefined,
messageRequestResponse: undefined,
});
// we can ignore the result of that markMessageReadNoCommit as it would only be used
// to refresh the expiry of it(but it is already marked as "deleted", so we don't care)

@ -54,7 +54,6 @@ import type { SetupI18nReturnType } from '../types/localizer'; // checked - only
import { StorageItem } from './storage_item'; // checked - only node
import {
AwaitedReturn,
CONFIG_DUMP_TABLE,
MsgDuplicateSearchOpenGroup,
roomHasBlindEnabled,
@ -66,10 +65,9 @@ import {
import { KNOWN_BLINDED_KEYS_ITEM, SettingsKey } from '../data/settings-key';
import {
DataCallArgs,
DeleteAllMessageFromSendersInConversationType,
DeleteAllMessageHashesInConversationMatchingAuthorType,
DeleteAllMessageHashesInConversationType,
FindAllMessageFromSendersInConversationTypeArgs,
FindAllMessageHashesInConversationMatchingAuthorTypeArgs,
FindAllMessageHashesInConversationTypeArgs,
} from '../data/sharedDataTypes';
import { MessageAttributes } from '../models/messageType';
import { SignalService } from '../protobuf';
@ -1097,72 +1095,68 @@ function removeAllMessagesInConversation(
.run({ conversationId });
}
function deleteAllMessageFromSendersInConversation(
{
groupPk,
toRemove,
signatureTimestamp,
}: DataCallArgs<DeleteAllMessageFromSendersInConversationType>,
function findAllMessageFromSendersInConversation(
{ groupPk, toRemove, signatureTimestamp }: FindAllMessageFromSendersInConversationTypeArgs,
instance?: BetterSqlite3.Database
): AwaitedReturn<DeleteAllMessageFromSendersInConversationType> {
) {
if (!groupPk || !toRemove.length) {
return { messageHashes: [] };
}
const messageHashes = compact(
assertGlobalInstanceOrInstance(instance)
.prepare(
`DELETE FROM ${MESSAGES_TABLE} WHERE conversationId = ? AND sent_at <= ? AND source IN ( ${toRemove.map(() => '?').join(', ')} ) RETURNING messageHash`
)
.all(groupPk, signatureTimestamp, ...toRemove)
.map(m => m.messageHash)
);
return { messageHashes };
const rows = assertGlobalInstanceOrInstance(instance)
.prepare(
`SELECT json FROM ${MESSAGES_TABLE} WHERE conversationId = ? AND sent_at <= ? AND source IN ( ${toRemove.map(() => '?').join(', ')} )`
)
.all(groupPk, signatureTimestamp, ...toRemove);
if (!rows || isEmpty(rows)) {
return [];
}
return map(rows, row => jsonToObject(row.json));
}
function deleteAllMessageHashesInConversation(
{
groupPk,
messageHashes,
signatureTimestamp,
}: DataCallArgs<DeleteAllMessageHashesInConversationType>,
function findAllMessageHashesInConversation(
{ groupPk, messageHashes, signatureTimestamp }: FindAllMessageHashesInConversationTypeArgs,
instance?: BetterSqlite3.Database
): AwaitedReturn<DeleteAllMessageHashesInConversationType> {
) {
if (!groupPk || !messageHashes.length) {
return { messageHashes: [] };
return [];
}
const deletedMessageHashes = compact(
const rows = compact(
assertGlobalInstanceOrInstance(instance)
.prepare(
`DELETE FROM ${MESSAGES_TABLE} WHERE conversationId = ? AND sent_at <= ? AND messageHash IN ( ${messageHashes.map(() => '?').join(', ')} ) RETURNING messageHash`
`SELECT json FROM ${MESSAGES_TABLE} WHERE conversationId = ? AND sent_at <= ? AND messageHash IN ( ${messageHashes.map(() => '?').join(', ')} )`
)
.all(groupPk, signatureTimestamp, ...messageHashes)
.map(m => m.messageHash)
);
return { messageHashes: deletedMessageHashes };
if (!rows || isEmpty(rows)) {
return [];
}
return map(rows, row => jsonToObject(row.json));
}
function deleteAllMessageHashesInConversationMatchingAuthor(
function findAllMessageHashesInConversationMatchingAuthor(
{
author,
groupPk,
messageHashes,
signatureTimestamp,
}: DataCallArgs<DeleteAllMessageHashesInConversationMatchingAuthorType>,
}: FindAllMessageHashesInConversationMatchingAuthorTypeArgs,
instance?: BetterSqlite3.Database
): AwaitedReturn<DeleteAllMessageHashesInConversationMatchingAuthorType> {
) {
if (!groupPk || !author || !messageHashes.length) {
return { msgHashesDeleted: [], msgIdsDeleted: [] };
}
const results = assertGlobalInstanceOrInstance(instance)
const rows = assertGlobalInstanceOrInstance(instance)
.prepare(
`DELETE FROM ${MESSAGES_TABLE} WHERE conversationId = ? AND source = ? AND sent_at <= ? AND messageHash IN ( ${messageHashes.map(() => '?').join(', ')} ) RETURNING id, messageHash;`
`SELECT json FROM ${MESSAGES_TABLE} WHERE conversationId = ? AND source = ? AND sent_at <= ? AND messageHash IN ( ${messageHashes.map(() => '?').join(', ')} );`
)
.all(groupPk, author, signatureTimestamp, ...messageHashes);
return {
msgHashesDeleted: results.map(m => m.messageHash),
msgIdsDeleted: results.map(m => m.id),
};
if (!rows || isEmpty(rows)) {
return null;
}
return map(rows, row => jsonToObject(row.json));
}
function cleanUpExpirationTimerUpdateHistory(
@ -2661,9 +2655,9 @@ export const sqlNode = {
getAllMessagesWithAttachmentsInConversationSentBefore,
cleanUpExpirationTimerUpdateHistory,
removeAllMessagesInConversation,
deleteAllMessageFromSendersInConversation,
deleteAllMessageHashesInConversation,
deleteAllMessageHashesInConversationMatchingAuthor,
findAllMessageFromSendersInConversation,
findAllMessageHashesInConversation,
findAllMessageHashesInConversationMatchingAuthor,
getUnreadByConversation,
getUnreadDisappearingByConversation,
markAllAsReadByConversationNoExpiration,

@ -1,5 +1,5 @@
import { GroupPubkeyType, PubkeyType, WithGroupPubkey } from 'libsession_util_nodejs';
import { compact, isEmpty, isFinite, isNumber } from 'lodash';
import { isEmpty, isFinite, isNumber } from 'lodash';
import { Data } from '../../data/data';
import { deleteAllMessagesByConvoIdNoConfirmation } from '../../interactions/conversationInteractions';
import { deleteMessagesFromSwarmOnly } from '../../interactions/conversations/unsendingInteractions';
@ -20,7 +20,6 @@ import { PreConditionFailed } from '../../session/utils/errors';
import { UserSync } from '../../session/utils/job_runners/jobs/UserSyncJob';
import { LibSessionUtil } from '../../session/utils/libsession/libsession_utils';
import { SessionUtilConvoInfoVolatile } from '../../session/utils/libsession/libsession_utils_convo_info_volatile';
import { messageHashesExpired, messagesExpired } from '../../state/ducks/conversations';
import { groupInfoActions } from '../../state/ducks/metaGroups';
import { toFixedUint8ArrayOfLength } from '../../types/sqlSharedTypes';
import { BlockedNumberController } from '../../util';
@ -80,7 +79,6 @@ async function getInitializedGroupObject({
};
}
found.kicked = false;
found.name = groupName;
if (groupSecretKey && !isEmpty(groupSecretKey)) {
found.secretKey = groupSecretKey;
@ -164,6 +162,7 @@ async function handleGroupUpdateInviteMessage({
found.authData = inviteMessage.memberAuthData;
await UserGroupsWrapperActions.setGroup(found);
await UserGroupsWrapperActions.markGroupInvited(groupPk);
// force markedAsUnread to be true so it shows the unread banner (we only show the banner if there are unread messages on at least one msg/group request)
await convo.markAsUnread(true, false);
await convo.commit();
@ -427,35 +426,45 @@ async function handleGroupDeleteMemberContentMessage({
* When `adminSignature` is not empty and valid,
* 2. we delete all the messages in the group sent by any of change.memberSessionIds AND
* 3. we delete all the messageHashes in the conversation matching the change.messageHashes (even if not from the right sender)
*
* Note: we never fully delete those messages locally, but only empty them and mark them as deleted with the
* "This message was deleted" placeholder.
* Eventually, we will be able to delete those "deleted by kept locally" messages with placeholders.
*/
if (isEmpty(change.adminSignature)) {
// no adminSignature: this was sent by a non-admin user
if (!change.adminSignature || isEmpty(change.adminSignature)) {
// this is step 1.
const { msgIdsDeleted, msgHashesDeleted } =
await Data.deleteAllMessageHashesInConversationMatchingAuthor({
author,
groupPk,
messageHashes: change.messageHashes,
signatureTimestamp,
});
window.inboxStore?.dispatch(
messagesExpired(msgIdsDeleted.map(m => ({ conversationKey: groupPk, messageId: m })))
);
const messageModels = await Data.findAllMessageHashesInConversationMatchingAuthor({
author,
groupPk,
messageHashes: change.messageHashes,
signatureTimestamp,
});
if (msgIdsDeleted.length) {
// Note: we `void` it because we don't want to hang while
// processing the handleGroupDeleteMemberContentMessage itself
// (we are running on the receiving pipeline here)
void deleteMessagesFromSwarmOnly(msgHashesDeleted, groupPk).catch(e => {
// we retry a bunch of times already, so if it still fails, there is not much we can do.
window.log.warn('deleteMessagesFromSwarmOnly failed with', e.message);
});
// we don't want to hang while for too long here
// processing the handleGroupDeleteMemberContentMessage itself
// (we are running on the receiving pipeline here)
// so network calls are not allowed.
for (let index = 0; index < messageModels.length; index++) {
const messageModel = messageModels[index];
try {
// eslint-disable-next-line no-await-in-loop
await messageModel.markAsDeleted();
} catch (e) {
window.log.warn(
`handleGroupDeleteMemberContentMessage markAsDeleted non-admin of ${messageModel.getMessageHash()} failed with`,
e.message
);
}
}
convo.updateLastMessage();
return;
}
// else case: we have an admin signature to verify
const sigValid = await verifySig({
pubKey: HexString.fromHexStringNoPrefix(groupPk),
signature: change.adminSignature,
@ -471,26 +480,36 @@ async function handleGroupDeleteMemberContentMessage({
const toRemove = change.memberSessionIds.filter(PubKey.is05Pubkey);
const deletedBySenders = await Data.deleteAllMessageFromSendersInConversation({
const modelsBySenders = await Data.findAllMessageFromSendersInConversation({
groupPk,
toRemove,
signatureTimestamp,
}); // this is step 2.
const deletedByHashes = await Data.deleteAllMessageHashesInConversation({
const modelsByHashes = await Data.findAllMessageHashesInConversation({
groupPk,
messageHashes: change.messageHashes,
signatureTimestamp,
}); // this is step 3.
window.inboxStore?.dispatch(
messageHashesExpired(
compact([...deletedByHashes.messageHashes, ...deletedBySenders.messageHashes]).map(m => ({
conversationKey: groupPk,
messageHash: m,
}))
)
);
// we don't want to hang while for too long here
// processing the handleGroupDeleteMemberContentMessage itself
// (we are running on the receiving pipeline here)
// so network calls are not allowed.
const mergedModels = modelsByHashes.concat(modelsBySenders);
for (let index = 0; index < mergedModels.length; index++) {
const messageModel = mergedModels[index];
try {
// eslint-disable-next-line no-await-in-loop
await messageModel.markAsDeleted();
} catch (e) {
window.log.warn(
`handleGroupDeleteMemberContentMessage markAsDeleted non-admin of ${messageModel.getMessageHash()} failed with`,
e.message
);
}
}
convo.updateLastMessage();
}
async function handleGroupUpdateInviteResponseMessage({

@ -57,6 +57,7 @@ import {
} from './types';
import { ConversationTypeEnum } from '../../../models/types';
import { Snode } from '../../../data/types';
import { isDevProd } from '../../../shared/env_vars';
const minMsgCountShouldRetry = 95;
@ -289,7 +290,9 @@ export class SwarmPolling {
if (!window.getGlobalOnlineStatus()) {
window?.log?.error('pollForAllKeys: offline');
// Very important to set up a new polling call so we do retry at some point
timeouts.push(setTimeout(this.pollForAllKeys.bind(this), SWARM_POLLING_TIMEOUT.ACTIVE));
timeouts.push(
setTimeout(this.pollForAllKeys.bind(this), isDevProd() ? 500 : SWARM_POLLING_TIMEOUT.ACTIVE)
);
return;
}
@ -309,7 +312,9 @@ export class SwarmPolling {
window?.log?.warn('pollForAllKeys exception: ', e);
throw e;
} finally {
timeouts.push(setTimeout(this.pollForAllKeys.bind(this), SWARM_POLLING_TIMEOUT.ACTIVE));
timeouts.push(
setTimeout(this.pollForAllKeys.bind(this), isDevProd() ? 500 : SWARM_POLLING_TIMEOUT.ACTIVE)
);
}
}

@ -317,9 +317,9 @@ class ConvoController {
}
await UserGroupsWrapperActions.setGroup(groupInUserGroup);
if (deletionType === 'keepAsKicked') {
await UserGroupsWrapperActions.setGroupKicked(groupPk);
await UserGroupsWrapperActions.markGroupKicked(groupPk);
} else {
await UserGroupsWrapperActions.setGroupDestroyed(groupPk);
await UserGroupsWrapperActions.markGroupDestroyed(groupPk);
}
}
} else {

@ -5,7 +5,6 @@ import { v4 } from 'uuid';
import { StringUtils } from '../..';
import { Data } from '../../../../data/data';
import { deleteMessagesFromSwarmOnly } from '../../../../interactions/conversations/unsendingInteractions';
import { messageHashesExpired } from '../../../../state/ducks/conversations';
import {
MetaGroupWrapperActions,
MultiEncryptWrapperActions,
@ -212,26 +211,27 @@ class GroupPendingRemovalsJob extends PersistedJob<GroupPendingRemovalsPersisted
try {
if (deleteMessagesOfMembers.length) {
const msgHashesToDeleteOnGroupSwarm =
await Data.deleteAllMessageFromSendersInConversation({
groupPk,
toRemove: deleteMessagesOfMembers,
signatureTimestamp: NetworkTime.now(),
});
debugger;
const models = await Data.findAllMessageFromSendersInConversation({
groupPk,
toRemove: deleteMessagesOfMembers,
signatureTimestamp: NetworkTime.now(),
});
if (msgHashesToDeleteOnGroupSwarm.messageHashes.length) {
const deleted = await deleteMessagesFromSwarmOnly(
msgHashesToDeleteOnGroupSwarm.messageHashes,
groupPk
);
if (deleted) {
window.inboxStore?.dispatch(
messageHashesExpired(
msgHashesToDeleteOnGroupSwarm.messageHashes.map(messageHash => ({
conversationKey: groupPk,
messageHash,
}))
)
const messageHashes = compact(models.map(m => m.getMessageHash()));
if (messageHashes.length) {
await deleteMessagesFromSwarmOnly(messageHashes, groupPk);
}
for (let index = 0; index < models.length; index++) {
const messageModel = models[index];
try {
// eslint-disable-next-line no-await-in-loop
await messageModel.markAsDeleted();
} catch (e) {
window.log.warn(
`GroupPendingRemoval markAsDeleted of ${messageModel.getMessageHash()} failed with`,
e.message
);
}
}

@ -244,7 +244,8 @@ class GroupSyncJob extends PersistedJob<GroupSyncPersistedData> {
// eslint-disable-next-line no-useless-catch
} catch (e) {
throw e;
window.log.warn('GroupSyncJob failed with', e.message);
return RunJobResult.RetryJobIfPossible;
} finally {
window.log.debug(
`GroupSyncJob ${ed25519Str(thisJobDestination)} run() took ${Date.now() - start}ms`

@ -173,7 +173,6 @@ async function insertGroupsFromDBIntoWrapperAndRefresh(
joinedAtSeconds: null, // no need to update this one except when we process an invite, maybe
name: null, // not updated except when we process an invite/create a group
secretKey: null, // not updated except when we process an promote/create a group
kicked: foundConvo.isKickedFromGroup() ?? null,
priority: foundConvo.getPriority() ?? null, // for 03 group, the priority is only tracked with libsession, so this is fine
};
try {

@ -17,6 +17,7 @@ import {
} from './conversations';
import { getLibMembersPubkeys, useLibGroupName } from './groups';
import { getCanWrite, getModerators, getSubscriberCount } from './sogsRoomInfo';
import { getLibGroupDestroyed, useLibGroupDestroyed } from './userGroups';
const getIsSelectedPrivate = (state: StateType): boolean => {
return Boolean(getSelectedConversation(state)?.isPrivate) || false;
@ -57,6 +58,7 @@ export const getSelectedConversationIsPublic = (state: StateType): boolean => {
*/
export function getSelectedCanWrite(state: StateType) {
const selectedConvoPubkey = getSelectedConversationKey(state);
const isSelectedGroupDestroyed = getLibGroupDestroyed(state, selectedConvoPubkey);
if (!selectedConvoPubkey) {
return false;
}
@ -69,9 +71,15 @@ export function getSelectedCanWrite(state: StateType) {
const readOnlySogs = isPublic && !canWriteSogs;
const isBlindedAndDisabledMsgRequests = getSelectedBlindedDisabledMsgRequests(state); // true if isPrivate, blinded and explicitely disabled msgreq
const isBlindedAndDisabledMsgRequests = getSelectedBlindedDisabledMsgRequests(state); // true if isPrivate, blinded and explicitly disabled msgreq
return !(isBlocked || isKickedFromGroup || readOnlySogs || isBlindedAndDisabledMsgRequests);
return !(
isBlocked ||
isKickedFromGroup ||
isSelectedGroupDestroyed ||
readOnlySogs ||
isBlindedAndDisabledMsgRequests
);
}
function getSelectedBlindedDisabledMsgRequests(state: StateType) {
@ -328,6 +336,11 @@ export function useSelectedIsKickedFromGroup() {
);
}
export function useSelectedIsGroupDestroyed() {
const convoKey = useSelectedConversationKey();
return useLibGroupDestroyed(convoKey);
}
export function useSelectedExpireTimer(): number | undefined {
return useSelector((state: StateType) => getSelectedConversation(state)?.expireTimer);
}

@ -39,3 +39,11 @@ export function getLibGroupKickedOutsideRedux(convoId?: string) {
return state ? getLibGroupKicked(state, convoId) : undefined;
}
export function getLibGroupDestroyed(state: StateType, convoId?: string) {
return getGroupById(state, convoId)?.destroyed;
}
export function useLibGroupDestroyed(convoId?: string) {
return useSelector((state: StateType) => getLibGroupDestroyed(state, convoId));
}

@ -341,28 +341,42 @@ export const UserGroupsWrapperActions: UserGroupsWrapperActionsCalls & {
ReturnType<UserGroupsWrapperActionsCalls['setGroup']>
>;
groups.set(group.pubkeyHex, group);
dispatchCachedGroupsToRedux();
return cloneDeep(group);
},
setGroupKicked: async (pubkeyHex: GroupPubkeyType) => {
markGroupKicked: async (pubkeyHex: GroupPubkeyType) => {
const group = (await callLibSessionWorker([
'UserGroupsConfig',
'setGroupKicked',
'markGroupKicked',
pubkeyHex,
])) as Awaited<ReturnType<UserGroupsWrapperActionsCalls['setGroupKicked']>>;
])) as Awaited<ReturnType<UserGroupsWrapperActionsCalls['markGroupKicked']>>;
groups.set(group.pubkeyHex, group);
dispatchCachedGroupsToRedux();
return cloneDeep(group);
},
setGroupDestroyed: async (pubkeyHex: GroupPubkeyType) => {
markGroupInvited: async (pubkeyHex: GroupPubkeyType) => {
const group = (await callLibSessionWorker([
'UserGroupsConfig',
'setGroupDestroyed',
'markGroupInvited',
pubkeyHex,
])) as Awaited<ReturnType<UserGroupsWrapperActionsCalls['setGroupDestroyed']>>;
])) as Awaited<ReturnType<UserGroupsWrapperActionsCalls['markGroupInvited']>>;
groups.set(group.pubkeyHex, group);
dispatchCachedGroupsToRedux();
return cloneDeep(group);
},
markGroupDestroyed: async (pubkeyHex: GroupPubkeyType) => {
const group = (await callLibSessionWorker([
'UserGroupsConfig',
'markGroupDestroyed',
pubkeyHex,
])) as Awaited<ReturnType<UserGroupsWrapperActionsCalls['markGroupDestroyed']>>;
groups.set(group.pubkeyHex, group);
dispatchCachedGroupsToRedux();
return cloneDeep(group);
},

Loading…
Cancel
Save