delete for opengroups is working

pull/1981/head
Audric Ackermann 3 years ago
parent 0ae23875b7
commit 0f2fcbb6e3
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -110,13 +110,14 @@
"continue": "Continue", "continue": "Continue",
"error": "Error", "error": "Error",
"delete": "Delete", "delete": "Delete",
"deletePublicWarning": "Are you sure? This will permanently remove this message for everyone in this open group.",
"deleteMultiplePublicWarning": "Are you sure? This will permanently remove these messages for everyone in this open group.",
"deleteWarning": "Are you sure? Clicking 'delete' will permanently remove this message from this device only.",
"deleteMultipleWarning": "Are you sure? Clicking 'delete' will permanently remove these messages from this device only.",
"messageDeletionForbidden": "You dont have permission to delete others messages", "messageDeletionForbidden": "You dont have permission to delete others messages",
"deleteThisMessage": "Delete message", "deleteJustForMe": "Delete just for me",
"deleteForEveryone": "Delete for everyone",
"deleteMessagesQuestion": "Delete those messages?",
"deleteMessageQuestion": "Delete this message?",
"deleteMessages": "Delete Messages",
"deleted": "Deleted", "deleted": "Deleted",
"messageDeletedPlaceholder": "This message has been deleted",
"from": "From:", "from": "From:",
"to": "To:", "to": "To:",
"sent": "Sent", "sent": "Sent",
@ -125,14 +126,6 @@
"groupMembers": "Group members", "groupMembers": "Group members",
"moreInformation": "More information", "moreInformation": "More information",
"resend": "Resend", "resend": "Resend",
"deleteMessage": "Delete Message",
"deleteMessageQuestion": "Delete message?",
"deleteMessagesQuestion": "Delete messages?",
"deleteMessages": "Delete Messages",
"deleteMessageForEveryone": "Delete Message For Everyone",
"deleteMessageForEveryoneLowercase": "Delete Message For Everyone",
"deleteMessagesForEveryone": "Delete Messages For Everyone",
"deleteForEveryone": "Delete for Everyone",
"deleteConversationConfirmation": "Permanently delete the messages in this conversation?", "deleteConversationConfirmation": "Permanently delete the messages in this conversation?",
"clearAllData": "Clear All Data", "clearAllData": "Clear All Data",
"deleteAccountWarning": "This will permanently delete your messages, and contacts.", "deleteAccountWarning": "This will permanently delete your messages, and contacts.",
@ -440,10 +433,6 @@
"recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.", "recoveryPhraseRevealMessage": "Secure your account by saving your recovery phrase. Reveal your recovery phrase then store it safely to secure it.",
"recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase",
"notificationSubtitle": "Notifications - $setting$", "notificationSubtitle": "Notifications - $setting$",
"deletionTypeTitle": "Deletion Type",
"deleteJustForMe": "Delete just for me",
"messageDeletedPlaceholder": "This message has been deleted",
"messageDeleted": "Message deleted",
"surveyTitle": "Take our Session Survey", "surveyTitle": "Take our Session Survey",
"goToOurSurvey": "Go to our survey", "goToOurSurvey": "Go to our survey",
"incomingCall": "Incoming call", "incomingCall": "Incoming call",

@ -78,7 +78,7 @@
} }
.message-selection-overlay .button-group { .message-selection-overlay .button-group {
float: right; display: flex;
} }
} }

@ -86,7 +86,7 @@ const HeaderItem = (props: {
const pinIcon = const pinIcon =
isMessagesSection && isPinned ? ( isMessagesSection && isPinned ? (
<SessionIcon iconType="pin" iconColor={'var(--color-text-subtle)'} iconSize={'tiny'} /> <SessionIcon iconType="pin" iconColor={'var(--color-text-subtle)'} iconSize={'small'} />
) : null; ) : null;
const NotificationSettingIcon = () => { const NotificationSettingIcon = () => {
@ -99,11 +99,11 @@ const HeaderItem = (props: {
return null; return null;
case 'disabled': case 'disabled':
return ( return (
<SessionIcon iconType="mute" iconColor={'var(--color-text-subtle)'} iconSize={'tiny'} /> <SessionIcon iconType="mute" iconColor={'var(--color-text-subtle)'} iconSize={'small'} />
); );
case 'mentions_only': case 'mentions_only':
return ( return (
<SessionIcon iconType="bell" iconColor={'var(--color-text-subtle)'} iconSize={'tiny'} /> <SessionIcon iconType="bell" iconColor={'var(--color-text-subtle)'} iconSize={'small'} />
); );
default: default:
return null; return null;

@ -15,6 +15,8 @@ import {
getConversationHeaderTitleProps, getConversationHeaderTitleProps,
getCurrentNotificationSettingText, getCurrentNotificationSettingText,
getSelectedConversation, getSelectedConversation,
getSelectedConversationIsPublic,
getSelectedConversationKey,
getSelectedMessageIds, getSelectedMessageIds,
isMessageDetailView, isMessageDetailView,
isMessageSelectionMode, isMessageSelectionMode,
@ -23,7 +25,10 @@ import {
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useMembersAvatars } from '../../hooks/useMembersAvatar'; import { useMembersAvatars } from '../../hooks/useMembersAvatar';
import { deleteMessagesById } from '../../interactions/conversationInteractions'; import {
deleteMessagesById,
deleteMessagesByIdForEveryone,
} from '../../interactions/conversations/unsendingInteractions';
import { import {
closeMessageDetailsView, closeMessageDetailsView,
closeRightPanel, closeRightPanel,
@ -67,16 +72,32 @@ export type ConversationHeaderProps = {
left: boolean; left: boolean;
}; };
const SelectionOverlay = (props: { const SelectionOverlay = () => {
onDeleteSelectedMessages: () => void; const selectedMessageIds = useSelector(getSelectedMessageIds);
onCloseOverlay: () => void; const selectedConversationKey = useSelector(getSelectedConversationKey);
isPublic: boolean; const isPublic = useSelector(getSelectedConversationIsPublic);
}) => { const dispatch = useDispatch();
const { onDeleteSelectedMessages, onCloseOverlay, isPublic } = props;
const { i18n } = window; const { i18n } = window;
const isServerDeletable = isPublic; function onCloseOverlay() {
const deleteMessageButtonText = i18n(isServerDeletable ? 'deleteForEveryone' : 'delete'); dispatch(resetSelectedMessageIds());
}
function onDeleteSelectedMessages() {
if (selectedConversationKey) {
void deleteMessagesById(selectedMessageIds, selectedConversationKey);
}
}
function onDeleteSelectedMessagesForEveryone() {
if (selectedConversationKey) {
void deleteMessagesByIdForEveryone(selectedMessageIds, selectedConversationKey);
}
}
const isOnlyServerDeletable = isPublic;
const deleteMessageButtonText = i18n('delete');
const deleteForEveroneMessageButtonText = i18n('deleteForEveryone');
return ( return (
<div className="message-selection-overlay"> <div className="message-selection-overlay">
@ -85,11 +106,19 @@ const SelectionOverlay = (props: {
</div> </div>
<div className="button-group"> <div className="button-group">
{!isOnlyServerDeletable && (
<SessionButton
buttonType={SessionButtonType.Default}
buttonColor={SessionButtonColor.Danger}
text={deleteMessageButtonText}
onClick={onDeleteSelectedMessages}
/>
)}
<SessionButton <SessionButton
buttonType={SessionButtonType.Default} buttonType={SessionButtonType.Default}
buttonColor={SessionButtonColor.Danger} buttonColor={SessionButtonColor.Danger}
text={deleteMessageButtonText} text={deleteForEveroneMessageButtonText}
onClick={onDeleteSelectedMessages} onClick={onDeleteSelectedMessagesForEveryone}
/> />
</div> </div>
</div> </div>
@ -285,7 +314,6 @@ export const ConversationHeaderWithDetails = () => {
const headerProps = useSelector(getConversationHeaderProps); const headerProps = useSelector(getConversationHeaderProps);
const isSelectionMode = useSelector(isMessageSelectionMode); const isSelectionMode = useSelector(isMessageSelectionMode);
const selectedMessageIds = useSelector(getSelectedMessageIds);
const selectedConversation = useSelector(getSelectedConversation); const selectedConversation = useSelector(getSelectedConversation);
const memberDetails = useMembersAvatars(selectedConversation); const memberDetails = useMembersAvatars(selectedConversation);
const isMessageDetailOpened = useSelector(isMessageDetailView); const isMessageDetailOpened = useSelector(isMessageDetailView);
@ -366,15 +394,7 @@ export const ConversationHeaderWithDetails = () => {
/> />
</div> </div>
{isSelectionMode && ( {isSelectionMode && <SelectionOverlay />}
<SelectionOverlay
isPublic={isPublic}
onCloseOverlay={() => dispatch(resetSelectedMessageIds())}
onDeleteSelectedMessages={() => {
void deleteMessagesById(selectedMessageIds, conversationKey, true);
}}
/>
)}
</div> </div>
); );
}; };

@ -5,13 +5,13 @@ import moment from 'moment';
import { Avatar, AvatarSize } from '../Avatar'; import { Avatar, AvatarSize } from '../Avatar';
import { ContactName } from './ContactName'; import { ContactName } from './ContactName';
import { Message } from './Message'; import { Message } from './Message';
import { deleteMessagesById } from '../../interactions/conversationInteractions';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { ContactPropsMessageDetail } from '../../state/ducks/conversations'; import { ContactPropsMessageDetail } from '../../state/ducks/conversations';
import { import {
getMessageDetailsViewProps, getMessageDetailsViewProps,
getMessageIsDeletable, getMessageIsDeletable,
} from '../../state/selectors/conversations'; } from '../../state/selectors/conversations';
import { deleteMessagesById } from '../../interactions/conversations/unsendingInteractions';
const AvatarItem = (props: { contact: ContactPropsMessageDetail }) => { const AvatarItem = (props: { contact: ContactPropsMessageDetail }) => {
const { avatarPath, pubkey, name, profileName } = props.contact; const { avatarPath, pubkey, name, profileName } = props.contact;
@ -26,12 +26,12 @@ const DeleteButtonItem = (props: { messageId: string; convoId: string; isDeletab
return props.isDeletable ? ( return props.isDeletable ? (
<div className="module-message-detail__delete-button-container"> <div className="module-message-detail__delete-button-container">
<button <button
onClick={() => { onClick={async () => {
void deleteMessagesById([props.messageId], props.convoId, true); await deleteMessagesById([props.messageId], props.convoId);
}} }}
className="module-message-detail__delete-button" className="module-message-detail__delete-button"
> >
{i18n('deleteThisMessage')} {i18n('delete')}
</button> </button>
</div> </div>
) : null; ) : null;

@ -35,24 +35,9 @@ export const MessageContentWithStatuses = (props: Props) => {
const onClickOnMessageOuterContainer = useCallback( const onClickOnMessageOuterContainer = useCallback(
(event: React.MouseEvent<HTMLDivElement>) => { (event: React.MouseEvent<HTMLDivElement>) => {
const selection = window.getSelection(); if (multiSelectMode && messageId) {
// Text is being selected event.preventDefault();
if (selection && selection.type === 'Range') { event.stopPropagation();
return;
}
// User clicked on message body
const target = event.target as HTMLDivElement;
if (
(!multiSelectMode && target.className === 'text-selectable') ||
window.contextMenuShown ||
props?.isDetailView
) {
return;
}
event.preventDefault();
event.stopPropagation();
if (messageId) {
dispatch(toggleSelectedMessageId(messageId)); dispatch(toggleSelectedMessageId(messageId));
} }
}, },

@ -4,7 +4,7 @@ import { animation, Item, Menu } from 'react-contexify';
import { MessageInteraction } from '../../../interactions'; import { MessageInteraction } from '../../../interactions';
import { getMessageById } from '../../../data/data'; import { getMessageById } from '../../../data/data';
import { deleteMessagesById, replyToMessage } from '../../../interactions/conversationInteractions'; import { replyToMessage } from '../../../interactions/conversationInteractions';
import { import {
showMessageDetailsView, showMessageDetailsView,
toggleSelectedMessageId, toggleSelectedMessageId,
@ -16,8 +16,12 @@ import {
} from '../../../interactions/messageInteractions'; } from '../../../interactions/messageInteractions';
import { MessageRenderingProps } from '../../../models/messageType'; import { MessageRenderingProps } from '../../../models/messageType';
import { pushUnblockToSend } from '../../../session/utils/Toast'; import { pushUnblockToSend } from '../../../session/utils/Toast';
import { useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { getMessageContextMenuProps } from '../../../state/selectors/conversations'; import { getMessageContextMenuProps } from '../../../state/selectors/conversations';
import {
deleteMessagesById,
deleteMessagesByIdForEveryone,
} from '../../../interactions/conversations/unsendingInteractions';
export type MessageContextMenuSelectorProps = Pick< export type MessageContextMenuSelectorProps = Pick<
MessageRenderingProps, MessageRenderingProps,
@ -35,6 +39,7 @@ export type MessageContextMenuSelectorProps = Pick<
| 'serverTimestamp' | 'serverTimestamp'
| 'timestamp' | 'timestamp'
| 'isBlocked' | 'isBlocked'
| 'isDeletableForEveryone'
>; >;
type Props = { messageId: string; contextMenuId: string }; type Props = { messageId: string; contextMenuId: string };
@ -42,6 +47,7 @@ type Props = { messageId: string; contextMenuId: string };
// tslint:disable: max-func-body-length cyclomatic-complexity // tslint:disable: max-func-body-length cyclomatic-complexity
export const MessageContextMenu = (props: Props) => { export const MessageContextMenu = (props: Props) => {
const selected = useSelector(state => getMessageContextMenuProps(state as any, props.messageId)); const selected = useSelector(state => getMessageContextMenuProps(state as any, props.messageId));
const dispatch = useDispatch();
if (!selected) { if (!selected) {
return null; return null;
@ -53,6 +59,7 @@ export const MessageContextMenu = (props: Props) => {
direction, direction,
status, status,
isDeletable, isDeletable,
isDeletableForEveryone,
isPublic, isPublic,
isOpenGroupV2, isOpenGroupV2,
weAreAdmin, weAreAdmin,
@ -85,14 +92,15 @@ export const MessageContextMenu = (props: Props) => {
const found = await getMessageById(messageId); const found = await getMessageById(messageId);
if (found) { if (found) {
const messageDetailsProps = await found.getPropsForMessageDetail(); const messageDetailsProps = await found.getPropsForMessageDetail();
window.inboxStore?.dispatch(showMessageDetailsView(messageDetailsProps)); dispatch(showMessageDetailsView(messageDetailsProps));
} else { } else {
window.log.warn(`Message ${messageId} not found in db`); window.log.warn(`Message ${messageId} not found in db`);
} }
}; };
const selectMessageText = window.i18n('selectMessage'); const selectMessageText = window.i18n('selectMessage');
const deleteMessageText = window.i18n('deleteMessage'); const deleteMessageJustForMeText = window.i18n('deleteJustForMe');
const unsendMessageText = window.i18n('deleteForEveryone');
const addModerator = useCallback(() => { const addModerator = useCallback(() => {
void addSenderAsModerator(authorPhoneNumber, convoId); void addSenderAsModerator(authorPhoneNumber, convoId);
@ -151,11 +159,15 @@ export const MessageContextMenu = (props: Props) => {
}, [authorPhoneNumber, convoId]); }, [authorPhoneNumber, convoId]);
const onSelect = useCallback(() => { const onSelect = useCallback(() => {
window.inboxStore?.dispatch(toggleSelectedMessageId(messageId)); dispatch(toggleSelectedMessageId(messageId));
}, [messageId]); }, [messageId]);
const onDelete = useCallback(() => { const onDelete = useCallback(() => {
void deleteMessagesById([messageId], convoId, true); void deleteMessagesById([messageId], convoId);
}, [convoId, messageId]);
const onDeleteForEveryone = useCallback(() => {
void deleteMessagesByIdForEveryone([messageId], convoId);
}, [convoId, messageId]); }, [convoId, messageId]);
return ( return (
@ -176,7 +188,16 @@ export const MessageContextMenu = (props: Props) => {
{isDeletable ? ( {isDeletable ? (
<> <>
<Item onClick={onSelect}>{selectMessageText}</Item> <Item onClick={onSelect}>{selectMessageText}</Item>
<Item onClick={onDelete}>{deleteMessageText}</Item> </>
) : null}
{isDeletable && !isPublic ? (
<>
<Item onClick={onDelete}>{deleteMessageJustForMeText}</Item>
</>
) : null}
{isDeletableForEveryone ? (
<>
<Item onClick={onDeleteForEveryone}>{unsendMessageText}</Item>
</> </>
) : null} ) : null}
{weAreAdmin && isPublic ? <Item onClick={onBan}>{window.i18n('banUser')}</Item> : null} {weAreAdmin && isPublic ? <Item onClick={onBan}>{window.i18n('banUser')}</Item> : null}

@ -13,7 +13,7 @@ import {
} from '../../../data/data'; } from '../../../data/data';
import { SpacerLG } from '../../basic/Text'; import { SpacerLG } from '../../basic/Text';
import { import {
deleteMessagesByConvoIdWithConfirmation, deleteAllMessagesByConvoIdWithConfirmation,
setDisappearingMessagesByConvoId, setDisappearingMessagesByConvoId,
showAddModeratorsByConvoId, showAddModeratorsByConvoId,
showInviteContactByConvoId, showInviteContactByConvoId,
@ -249,7 +249,7 @@ export const SessionRightPanelWithDetails = () => {
const deleteConvoAction = isPublic const deleteConvoAction = isPublic
? () => { ? () => {
deleteMessagesByConvoIdWithConfirmation(id); deleteAllMessagesByConvoIdWithConfirmation(id);
} }
: () => { : () => {
showLeaveGroupByConvoId(id); showLeaveGroupByConvoId(id);

@ -78,7 +78,7 @@ const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => {
{getMarkAllReadMenuItem(conversationId)} {getMarkAllReadMenuItem(conversationId)}
{getChangeNicknameMenuItem(isMe, isGroup, conversationId)} {getChangeNicknameMenuItem(isMe, isGroup, conversationId)}
{getClearNicknameMenuItem(isMe, hasNickname, isGroup, conversationId)} {getClearNicknameMenuItem(isMe, hasNickname, isGroup, conversationId)}
{getDeleteMessagesMenuItem(isPublic, conversationId)} {getDeleteMessagesMenuItem(conversationId)}
{getAddModeratorsMenuItem(weAreAdmin, isPublic, isKickedFromGroup, conversationId)} {getAddModeratorsMenuItem(weAreAdmin, isPublic, isKickedFromGroup, conversationId)}
{getRemoveModeratorsMenuItem(weAreAdmin, isPublic, isKickedFromGroup, conversationId)} {getRemoveModeratorsMenuItem(weAreAdmin, isPublic, isKickedFromGroup, conversationId)}
{getUpdateGroupNameMenuItem(weAreAdmin, isKickedFromGroup, left, conversationId)} {getUpdateGroupNameMenuItem(weAreAdmin, isKickedFromGroup, left, conversationId)}

@ -76,7 +76,7 @@ const ConversationListItemContextMenu = (props: PropsContextConversationItem) =>
{getMarkAllReadMenuItem(conversationId)} {getMarkAllReadMenuItem(conversationId)}
{getChangeNicknameMenuItem(isMe, isGroup, conversationId)} {getChangeNicknameMenuItem(isMe, isGroup, conversationId)}
{getClearNicknameMenuItem(isMe, hasNickname, isGroup, conversationId)} {getClearNicknameMenuItem(isMe, hasNickname, isGroup, conversationId)}
{getDeleteMessagesMenuItem(isPublic, conversationId)} {getDeleteMessagesMenuItem(conversationId)}
{getInviteContactMenuItem(isGroup, isPublic, conversationId)} {getInviteContactMenuItem(isGroup, isPublic, conversationId)}
{getDeleteContactMenuItem(isGroup, isPublic, left, isKickedFromGroup, conversationId)} {getDeleteContactMenuItem(isGroup, isPublic, left, isKickedFromGroup, conversationId)}
{getLeaveGroupMenuItem(isKickedFromGroup, left, isGroup, isPublic, conversationId)} {getLeaveGroupMenuItem(isKickedFromGroup, left, isGroup, isPublic, conversationId)}

@ -23,7 +23,7 @@ import {
blockConvoById, blockConvoById,
clearNickNameByConvoId, clearNickNameByConvoId,
copyPublicKeyByConvoId, copyPublicKeyByConvoId,
deleteMessagesByConvoIdWithConfirmation, deleteAllMessagesByConvoIdWithConfirmation,
markAllReadByConvoId, markAllReadByConvoId,
setDisappearingMessagesByConvoId, setDisappearingMessagesByConvoId,
setNotificationForConvoId, setNotificationForConvoId,
@ -70,10 +70,6 @@ function showChangeNickname(isMe: boolean, isGroup: boolean) {
return !isMe && !isGroup; return !isMe && !isGroup;
} }
function showDeleteMessages(isPublic: boolean): boolean {
return !isPublic;
}
// we want to show the copyId for open groups and private chats only // we want to show the copyId for open groups and private chats only
function showCopyId(isPublic: boolean, isGroup: boolean): boolean { function showCopyId(isPublic: boolean, isGroup: boolean): boolean {
return !isGroup || isPublic; return !isGroup || isPublic;
@ -546,20 +542,16 @@ export function getChangeNicknameMenuItem(
return null; return null;
} }
export function getDeleteMessagesMenuItem( export function getDeleteMessagesMenuItem(conversationId: string): JSX.Element | null {
isPublic: boolean | undefined, return (
conversationId: string <Item
): JSX.Element | null { onClick={() => {
if (showDeleteMessages(Boolean(isPublic))) { deleteAllMessagesByConvoIdWithConfirmation(conversationId);
return ( }}
<Item >
onClick={() => { {window.i18n('deleteMessages')}
deleteMessagesByConvoIdWithConfirmation(conversationId); </Item>
}} );
>
{window.i18n('deleteMessages')}
</Item>
);
}
return null; return null;
} }

@ -5,13 +5,7 @@ import {
} from '../opengroup/utils/OpenGroupUtils'; } from '../opengroup/utils/OpenGroupUtils';
import { getV2OpenGroupRoom } from '../data/opengroups'; import { getV2OpenGroupRoom } from '../data/opengroups';
import { SyncUtils, ToastUtils, UserUtils } from '../session/utils'; import { SyncUtils, ToastUtils, UserUtils } from '../session/utils';
import { import { ConversationNotificationSettingType, ConversationTypeEnum } from '../models/conversation';
ConversationModel,
ConversationNotificationSettingType,
ConversationTypeEnum,
} from '../models/conversation';
import { MessageModel } from '../models/message';
import { ApiV2 } from '../opengroup/opengroupV2';
import _ from 'lodash'; import _ from 'lodash';
import { getConversationController } from '../session/conversations'; import { getConversationController } from '../session/conversations';
@ -32,11 +26,7 @@ import {
lastAvatarUploadTimestamp, lastAvatarUploadTimestamp,
removeAllMessagesInConversation, removeAllMessagesInConversation,
} from '../data/data'; } from '../data/data';
import { import { conversationReset, quoteMessage } from '../state/ducks/conversations';
conversationReset,
quoteMessage,
resetSelectedMessageIds,
} from '../state/ducks/conversations';
import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsManager'; import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsManager';
import { IMAGE_JPEG } from '../types/MIME'; import { IMAGE_JPEG } from '../types/MIME';
import { FSv2 } from '../fileserver'; import { FSv2 } from '../fileserver';
@ -86,61 +76,6 @@ export async function copyPublicKeyByConvoId(convoId: string) {
ToastUtils.pushCopiedToClipBoard(); ToastUtils.pushCopiedToClipBoard();
} }
/**
*
* @param messages the list of MessageModel to delete
* @param convo the conversation to delete from (only v2 opengroups are supported)
*/
async function deleteOpenGroupMessages(
messages: Array<MessageModel>,
convo: ConversationModel
): Promise<Array<string>> {
if (!convo.isPublic()) {
throw new Error('cannot delete public message on a non public groups');
}
if (convo.isOpenGroupV2()) {
const roomInfos = convo.toOpenGroupV2();
// on v2 servers we can only remove a single message per request..
// so logic here is to delete each messages and get which one where not removed
const validServerIdsToRemove = _.compact(
messages.map(msg => {
return msg.get('serverId');
})
);
const validMessageModelsToRemove = _.compact(
messages.map(msg => {
const serverId = msg.get('serverId');
if (serverId) {
return msg;
}
return undefined;
})
);
let allMessagesAreDeleted: boolean = false;
if (validServerIdsToRemove.length) {
allMessagesAreDeleted = await ApiV2.deleteMessageByServerIds(
validServerIdsToRemove,
roomInfos
);
}
// remove only the messages we managed to remove on the server
if (allMessagesAreDeleted) {
window?.log?.info('Removed all those serverIds messages successfully');
return validMessageModelsToRemove.map(m => m.id as string);
} else {
window?.log?.info(
'failed to remove all those serverIds message. not removing them locally neither'
);
return [];
}
} else {
throw new Error('Opengroupv1 are not supported anymore');
}
}
export async function blockConvoById(conversationId: string) { export async function blockConvoById(conversationId: string) {
const conversation = getConversationController().get(conversationId); const conversation = getConversationController().get(conversationId);
@ -286,7 +221,7 @@ export function showChangeNickNameByConvoId(conversationId: string) {
window.inboxStore?.dispatch(changeNickNameModal({ conversationId })); window.inboxStore?.dispatch(changeNickNameModal({ conversationId }));
} }
export async function deleteMessagesByConvoIdNoConfirmation(conversationId: string) { export async function deleteAllMessagesByConvoIdNoConfirmation(conversationId: string) {
const conversation = getConversationController().get(conversationId); const conversation = getConversationController().get(conversationId);
await removeAllMessagesInConversation(conversationId); await removeAllMessagesInConversation(conversationId);
window.inboxStore?.dispatch(conversationReset(conversationId)); window.inboxStore?.dispatch(conversationReset(conversationId));
@ -302,13 +237,13 @@ export async function deleteMessagesByConvoIdNoConfirmation(conversationId: stri
await conversation.commit(); await conversation.commit();
} }
export function deleteMessagesByConvoIdWithConfirmation(conversationId: string) { export function deleteAllMessagesByConvoIdWithConfirmation(conversationId: string) {
const onClickClose = () => { const onClickClose = () => {
window?.inboxStore?.dispatch(updateConfirmModal(null)); window?.inboxStore?.dispatch(updateConfirmModal(null));
}; };
const onClickOk = async () => { const onClickOk = async () => {
await deleteMessagesByConvoIdNoConfirmation(conversationId); await deleteAllMessagesByConvoIdNoConfirmation(conversationId);
onClickClose(); onClickClose();
}; };
@ -435,196 +370,6 @@ export async function uploadOurAvatar(newAvatarDecrypted?: ArrayBuffer) {
} }
} }
/**
* Deletes messages for everyone in a 1-1 or closed group conversation
* @param msgsToDelete Messages to delete
*/
async function deleteForAll(conversation: ConversationModel, msgsToDelete: Array<MessageModel>) {
window?.log?.warn('Deleting messages for all users in this conversation');
const result = await conversation.unsendMessages(msgsToDelete);
// TODO: may need to specify deletion for own device as well.
window.inboxStore?.dispatch(resetSelectedMessageIds());
if (result) {
ToastUtils.pushDeleted();
} else {
ToastUtils.someDeletionsFailed();
}
}
/**
*
* @param toDeleteLocallyIds Messages to delete for just this user. Still sends an unsend message to sync
* with other devices
*/
async function deleteForJustThisUser(
conversation: ConversationModel,
msgsToDelete: Array<MessageModel>
) {
window?.log?.warn('Deleting messages just for this user');
// is deleting on swarm sufficient or does it need to be unsent as well?
const deleteResult = await conversation.deleteMessages(msgsToDelete);
// Update view and trigger update
window.inboxStore?.dispatch(resetSelectedMessageIds());
if (deleteResult) {
ToastUtils.pushDeleted();
} else {
ToastUtils.someDeletionsFailed();
}
}
const doDeleteMessagesById = async (
selectedMessages: Array<MessageModel>,
conversation: ConversationModel,
deleteForEveryone: boolean = true
) => {
let toDeleteLocallyIds: Array<string>;
const ourDevicePubkey = UserUtils.getOurPubKeyStrFromCache();
if (!ourDevicePubkey) {
return;
}
const isServerDeletable = conversation.isPublic();
const isAllOurs = selectedMessages.every(message => ourDevicePubkey === message.getSource());
if (isServerDeletable) {
//#region open group v2 deletion
// Get our Moderator status
const isAdmin = conversation.isAdmin(ourDevicePubkey);
if (!isAllOurs && !isAdmin) {
ToastUtils.pushMessageDeleteForbidden();
window.inboxStore?.dispatch(resetSelectedMessageIds());
return;
}
toDeleteLocallyIds = await deleteOpenGroupMessages(selectedMessages, conversation);
if (toDeleteLocallyIds.length === 0) {
// Message failed to delete from server, show error?
return;
}
// successful deletion
ToastUtils.pushDeleted();
window.inboxStore?.dispatch(resetSelectedMessageIds());
//#endregion
} else {
//#region deletion for 1-1 and closed groups
if (!isAllOurs) {
ToastUtils.pushMessageDeleteForbidden();
window.inboxStore?.dispatch(resetSelectedMessageIds());
return;
}
if (window.lokiFeatureFlags?.useUnsendRequests) {
if (deleteForEveryone) {
void deleteForAll(conversation, selectedMessages);
} else {
void deleteForJustThisUser(conversation, selectedMessages);
}
} else {
//#region to remove once unsend enabled
const messageIds = selectedMessages.map(m => m.id) as Array<string>;
await Promise.all(messageIds.map(msgId => conversation.removeMessage(msgId)));
ToastUtils.pushDeleted();
window.inboxStore?.dispatch(resetSelectedMessageIds());
//#endregion
}
//#endregion
}
};
// tslint:disable-next-line: max-func-body-length
export async function deleteMessagesById(
messageIds: Array<string>,
conversationId: string,
askUserForConfirmation: boolean
) {
const conversation = getConversationController().getOrThrow(conversationId);
const selectedMessages = _.compact(
await Promise.all(messageIds.map(m => getMessageById(m, false)))
);
const moreThanOne = selectedMessages.length > 1;
// In future, we may be able to unsend private messages also
// isServerDeletable also defined in ConversationHeader.tsx for
// future reference
const isServerDeletable = conversation.isPublic();
if (askUserForConfirmation) {
let title = '';
// Note: keep that i18n logic separated so the scripts in tools/ find the usage of those
if (isServerDeletable) {
if (moreThanOne) {
title = window.i18n('deleteMessagesForEveryone');
} else {
title = window.i18n('deleteMessageForEveryone');
}
} else {
if (moreThanOne) {
title = window.i18n('deleteMessages');
} else {
title = window.i18n('deleteMessage');
}
}
const okText = window.i18n(isServerDeletable ? 'deleteForEveryone' : 'delete');
//#region confirmation for deletion of messages
const showDeletionTypeModal = () => {
window.inboxStore?.dispatch(updateConfirmModal(null));
window.inboxStore?.dispatch(
updateConfirmModal({
showExitIcon: true,
title: window.i18n('deletionTypeTitle'),
okText: window.i18n('deleteMessageForEveryoneLowercase'),
okTheme: SessionButtonColor.Danger,
onClickOk: async () => {
await doDeleteMessagesById(selectedMessages, conversation, true);
},
cancelText: window.i18n('deleteJustForMe'),
onClickCancel: async () => {
await doDeleteMessagesById(selectedMessages, conversation, false);
},
onClickClose: () => {
window.inboxStore?.dispatch(updateConfirmModal(null));
},
})
);
return;
};
window.inboxStore?.dispatch(
updateConfirmModal({
title,
message: window.i18n(moreThanOne ? 'deleteMessagesQuestion' : 'deleteMessageQuestion'),
okText,
okTheme: SessionButtonColor.Danger,
onClickOk: async () => {
if (isServerDeletable) {
// unsend logic
await doDeleteMessagesById(selectedMessages, conversation, true);
// explicity close modal for this case.
window.inboxStore?.dispatch(updateConfirmModal(null));
return;
}
if (window.lokiFeatureFlags?.useUnsendRequests) {
showDeletionTypeModal();
} else {
await doDeleteMessagesById(selectedMessages, conversation, false);
window.inboxStore?.dispatch(updateConfirmModal(null));
}
},
closeAfterInput: false,
})
);
//#endregion
} else {
void doDeleteMessagesById(selectedMessages, conversation);
}
}
export async function replyToMessage(messageId: string) { export async function replyToMessage(messageId: string) {
const quotedMessageModel = await getMessageById(messageId); const quotedMessageModel = await getMessageById(messageId);
if (!quotedMessageModel) { if (!quotedMessageModel) {

@ -0,0 +1,366 @@
import _ from 'underscore';
import { SessionButtonColor } from '../../components/session/SessionButton';
import { getMessageById } from '../../data/data';
import { ConversationModel } from '../../models/conversation';
import { MessageModel } from '../../models/message';
import { ApiV2 } from '../../opengroup/opengroupV2';
import { getMessageQueue } from '../../session';
import { getConversationController } from '../../session/conversations';
import { UnsendMessage } from '../../session/messages/outgoing/controlMessage/UnsendMessage';
import { networkDeleteMessages } from '../../session/snode_api/SNodeAPI';
import { PubKey } from '../../session/types';
import { ToastUtils, UserUtils } from '../../session/utils';
import { resetSelectedMessageIds } from '../../state/ducks/conversations';
import { updateConfirmModal } from '../../state/ducks/modalDialog';
/**
* Deletes messages for everyone in a 1-1 or everyone in a closed group conversation.
*/
async function unsendMessagesForEveryone(
conversation: ConversationModel,
msgsToDelete: Array<MessageModel>
) {
window?.log?.info('Deleting messages for all users in this conversation');
const destinationId = conversation.id;
if (!destinationId) {
return;
}
if (conversation.isOpenGroupV2()) {
throw new Error(
'Cannot unsend a message for an opengroup v2. This has to be a deleteMessage api call'
);
}
const unsendMsgObjects = getUnsendMessagesObjects(msgsToDelete);
let allDeleted = false;
if (conversation.isPrivate()) {
// sending to recipient all the messages separately for now
await Promise.all(
unsendMsgObjects.map(unsendObject =>
getMessageQueue()
.sendToPubKey(new PubKey(destinationId), unsendObject)
.catch(window?.log?.error)
)
);
allDeleted = await deleteMessagesFromSwarmAndCompletelyLocally(conversation, msgsToDelete);
} else if (conversation.isClosedGroup()) {
// sending to recipient all the messages separately for now
await Promise.all(
unsendMsgObjects.map(unsendObject =>
getMessageQueue()
.sendToGroup(unsendObject, undefined, new PubKey(destinationId))
.catch(window?.log?.error)
)
);
allDeleted = await deleteMessagesFromSwarmAndCompletelyLocally(conversation, msgsToDelete);
return;
}
window.inboxStore?.dispatch(resetSelectedMessageIds());
if (allDeleted) {
ToastUtils.pushDeleted();
} else {
ToastUtils.someDeletionsFailed();
}
}
function getUnsendMessagesObjects(messages: Array<MessageModel>) {
//#region building request
return _.compact(
messages.map(message => {
const author = message.get('source');
// call getPropsForMessage here so we get the received_at or sent_at timestamp in timestamp
const timestamp = message.getPropsForMessage().timestamp;
if (!timestamp) {
window?.log?.error('cannot find timestamp - aborting unsend request');
return undefined;
}
const unsendParams = {
timestamp,
author,
};
return new UnsendMessage(unsendParams);
})
);
//#endregion
}
/**
* Do a single request to the swarm with all the message hashes to delete from the swarm.
*
* It does not delete anything locally.
*/
export async function deleteMessagesFromSwarmOnly(messages: Array<MessageModel>) {
try {
const deletionMessageHashes = _.compact(messages.map(m => m.get('messageHash')));
if (deletionMessageHashes.length > 0) {
await networkDeleteMessages(deletionMessageHashes);
}
} catch (e) {
window.log?.error('Error deleting message from swarm', e);
return false;
}
return true;
}
/**
* Delete the messages from the swarm with an unsend request and if it worked, delete those messages locally.
* If an error happened, we just return false, Toast an error, and do not remove the messages locally at all.
*/
async function deleteMessagesFromSwarmAndCompletelyLocally(
conversation: ConversationModel,
messages: Array<MessageModel>
) {
const deletedFromSwarm = await deleteMessagesFromSwarmOnly(messages);
if (!deletedFromSwarm) {
window.log.warn(
'deleteMessagesFromSwarmAndCompletelyLocally: some messages failed to be deleted '
);
return false;
}
await Promise.all(
messages.map(async message => {
return deleteMessageLocallyOnly({ conversation, message, deletionType: 'complete' });
})
);
return true;
}
/**
* Deletes a message completely or mark it as deleted only. Does not interact with the swarm at all
* @param message Message to delete
* @param deletionType 'complete' means completely delete the item from the database, markDeleted means empty the message content but keep an entry
*/
export async function deleteMessageLocallyOnly({
conversation,
message,
deletionType,
}: {
conversation: ConversationModel;
message: MessageModel;
deletionType: 'complete' | 'markDeleted';
}) {
if (deletionType === 'complete') {
// remove the message from the database
await conversation.removeMessage(message.get('id'));
} else {
// just mark the message as deleted but still show in conversation
await message.markAsDeleted();
await message.markRead(Date.now());
}
conversation.updateLastMessage();
}
/**
*
*/
async function deleteJustForThisUser(
conversation: ConversationModel,
msgsToDelete: Array<MessageModel>
) {
window?.log?.warn('Deleting messages just for this user');
// is deleting on swarm sufficient or does it need to be unsent as well?
const deleteResult = await deleteMessagesFromSwarmAndCompletelyLocally(
conversation,
msgsToDelete
);
// Update view and trigger update
window.inboxStore?.dispatch(resetSelectedMessageIds());
if (deleteResult) {
ToastUtils.pushDeleted();
} else {
ToastUtils.someDeletionsFailed();
}
}
const doDeleteSelectedMessagesInSOGS = async (
selectedMessages: Array<MessageModel>,
conversation: ConversationModel,
isAllOurs: boolean
) => {
const ourDevicePubkey = UserUtils.getOurPubKeyStrFromCache();
if (!ourDevicePubkey) {
return;
}
//#region open group v2 deletion
// Get our Moderator status
const isAdmin = conversation.isAdmin(ourDevicePubkey);
if (!isAllOurs && !isAdmin) {
ToastUtils.pushMessageDeleteForbidden();
window.inboxStore?.dispatch(resetSelectedMessageIds());
return;
}
const toDeleteLocallyIds = await deleteOpenGroupMessages(selectedMessages, conversation);
if (toDeleteLocallyIds.length === 0) {
// Message failed to delete from server, show error?
return;
}
await Promise.all(
toDeleteLocallyIds.map(async id => {
const msgToDeleteLocally = await getMessageById(id);
if (msgToDeleteLocally) {
return deleteMessageLocallyOnly({
conversation,
message: msgToDeleteLocally,
deletionType: 'complete',
});
}
})
);
// successful deletion
ToastUtils.pushDeleted();
window.inboxStore?.dispatch(resetSelectedMessageIds());
//#endregion
};
/**
* Effectively delete the messages from a conversation.
* This call is to be called by the user on a confirmation dialog for instance.
*
* It does what needs to be done on a user action to delete messages for each conversation type
*/
const doDeleteSelectedMessages = async (
selectedMessages: Array<MessageModel>,
conversation: ConversationModel,
shouldDeleteForEveryone: boolean
) => {
const ourDevicePubkey = UserUtils.getOurPubKeyStrFromCache();
if (!ourDevicePubkey) {
return;
}
const isAllOurs = selectedMessages.every(message => ourDevicePubkey === message.getSource());
if (conversation.isPublic()) {
return doDeleteSelectedMessagesInSOGS(selectedMessages, conversation, isAllOurs);
}
//#region deletion for 1-1 and closed groups
if (!isAllOurs) {
ToastUtils.pushMessageDeleteForbidden();
window.inboxStore?.dispatch(resetSelectedMessageIds());
return;
}
if (shouldDeleteForEveryone) {
return unsendMessagesForEveryone(conversation, selectedMessages);
}
return deleteJustForThisUser(conversation, selectedMessages);
//#endregion
};
// tslint:disable-next-line: max-func-body-length
export async function deleteMessagesByIdForEveryone(
messageIds: Array<string>,
conversationId: string
) {
const conversation = getConversationController().getOrThrow(conversationId);
const selectedMessages = _.compact(
await Promise.all(messageIds.map(m => getMessageById(m, false)))
);
const moreThanOne = selectedMessages.length > 1;
window.inboxStore?.dispatch(
updateConfirmModal({
title: window.i18n('deleteForEveryone'),
message: moreThanOne
? window.i18n('deleteMessagesQuestion')
: window.i18n('deleteMessageQuestion'),
okText: window.i18n('deleteForEveryone'),
okTheme: SessionButtonColor.Danger,
onClickOk: async () => {
await doDeleteSelectedMessages(selectedMessages, conversation, true);
// explicity close modal for this case.
window.inboxStore?.dispatch(updateConfirmModal(null));
return;
},
closeAfterInput: false,
})
);
}
// tslint:disable-next-line: max-func-body-length
export async function deleteMessagesById(messageIds: Array<string>, conversationId: string) {
const conversation = getConversationController().getOrThrow(conversationId);
const selectedMessages = _.compact(
await Promise.all(messageIds.map(m => getMessageById(m, false)))
);
const moreThanOne = selectedMessages.length > 1;
window.inboxStore?.dispatch(
updateConfirmModal({
title: window.i18n('deleteJustForMe'),
message: moreThanOne
? window.i18n('deleteMessagesQuestion')
: window.i18n('deleteMessageQuestion'),
okText: window.i18n('delete'),
okTheme: SessionButtonColor.Danger,
onClickOk: async () => {
await doDeleteSelectedMessages(selectedMessages, conversation, false);
},
closeAfterInput: false,
})
);
}
/**
*
* @param messages the list of MessageModel to delete
* @param convo the conversation to delete from (only v2 opengroups are supported)
*/
async function deleteOpenGroupMessages(
messages: Array<MessageModel>,
convo: ConversationModel
): Promise<Array<string>> {
if (!convo.isPublic()) {
throw new Error('cannot delete public message on a non public groups');
}
if (!convo.isOpenGroupV2()) {
throw new Error('Opengroupv1 are not supported anymore');
}
const roomInfos = convo.toOpenGroupV2();
// on v2 servers we can only remove a single message per request..
// so logic here is to delete each messages and get which one where not removed
const validServerIdsToRemove = _.compact(
messages.map(msg => {
return msg.get('serverId');
})
);
const validMessageModelsToRemove = _.compact(
messages.map(msg => {
const serverId = msg.get('serverId');
if (serverId) {
return msg;
}
return undefined;
})
);
let allMessagesAreDeleted: boolean = false;
if (validServerIdsToRemove.length) {
allMessagesAreDeleted = await ApiV2.deleteMessageByServerIds(validServerIdsToRemove, roomInfos);
}
// remove only the messages we managed to remove on the server
if (allMessagesAreDeleted) {
window?.log?.info('Removed all those serverIds messages successfully');
return validMessageModelsToRemove.map(m => m.id as string);
} else {
window?.log?.info(
'failed to remove all those serverIds message. not removing them locally neither'
);
return [];
}
}

@ -48,8 +48,7 @@ import {
import { ed25519Str } from '../session/onions/onionPath'; import { ed25519Str } from '../session/onions/onionPath';
import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsManager'; import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsManager';
import { IMAGE_JPEG } from '../types/MIME'; import { IMAGE_JPEG } from '../types/MIME';
import { UnsendMessage } from '../session/messages/outgoing/controlMessage/UnsendMessage'; import { getLatestTimestampOffset } from '../session/snode_api/SNodeAPI';
import { getLatestTimestampOffset, networkDeleteMessages } from '../session/snode_api/SNodeAPI';
export enum ConversationTypeEnum { export enum ConversationTypeEnum {
GROUP = 'group', GROUP = 'group',
@ -800,127 +799,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
} }
} }
/**
* @param messages Messages to delete
*/
public async deleteMessages(messages: Array<MessageModel>) {
const results = await Promise.all(
messages.map(async message => {
return this.deleteMessage(message, true);
})
);
return _.every(results);
}
/**
* Deletes message from this device's swarm and handles local deletion of message
* @param message Message to delete
* @param removeFromDatabase delete message from the database entirely or just modify the message data
* @returns boolean if the deletion succeeeded
*/
public async deleteMessage(message: MessageModel, removeFromDatabase = false): Promise<boolean> {
//#region deletion on network
try {
const deletionMessageHashes = _.compact([message.get('messageHash')]);
if (deletionMessageHashes.length > 0) {
await networkDeleteMessages(deletionMessageHashes);
}
} catch (e) {
window.log?.error('Error deleting message from swarm', e);
return false;
}
//#endregion
//#region handling database
if (removeFromDatabase) {
// remove the message from the database
await this.removeMessage(message.get('id'));
} else {
// just mark the message as deleted but still show in conversation
await message.markAsDeleted();
await message.markRead(Date.now());
this.updateLastMessage();
}
//#endregion
return true;
}
public async unsendMessages(messages: Array<MessageModel>) {
const results = await Promise.all(
messages.map(async message => {
return this.unsendMessage(message, false);
})
);
return _.every(results);
}
/**
* Creates an unsend request using protobuf and adds to messageQueue.
* @param message Message to unsend
*/
public async unsendMessage(
message: MessageModel,
onlyDeleteForSender: boolean = false
): Promise<boolean> {
if (!message.get('messageHash')) {
window?.log?.error(`message ${message.id}, cannot find hash: ${message.get('messageHash')}`);
return false;
}
const ownPrimaryDevicePubkey = UserUtils.getOurPubKeyFromCache();
// If deleting just for sender, set destination to sender
const destinationId = onlyDeleteForSender ? ownPrimaryDevicePubkey : this.id;
if (!destinationId) {
return false;
}
//#endregion
//#region building request
const author = message.get('source');
// call getPropsForMessage here so we get the received_at or sent_at timestamp in timestamp
const timestamp = message.getPropsForMessage().timestamp;
if (!timestamp) {
window?.log?.error('cannot find timestamp - aborting unsend request');
return false;
}
const unsendParams = {
timestamp,
author,
};
const unsendMessage = new UnsendMessage(unsendParams);
//#endregion
//#region sending
// 1-1 Session
if (!this.isGroup()) {
// sending to recipient
await getMessageQueue()
.sendToPubKey(new PubKey(destinationId), unsendMessage)
.catch(window?.log?.error);
return this.deleteMessage(message);
}
// closed groups
if (this.isClosedGroup() && this.id) {
await getMessageQueue()
.sendToGroup(unsendMessage, undefined, PubKey.cast(this.id))
.catch(window?.log?.error);
// not calling deleteMessage as it'll be called by the unsend handler when it's received
return true;
}
// open groups
if (this.isOpenGroupV2()) {
window?.log?.info('Conversation is open group. Skipping unsend request.');
}
return true;
//#endregion
}
public async sendMessage(msg: SendMessageType) { public async sendMessage(msg: SendMessageType) {
const { attachments, body, groupInvitation, preview, quote } = msg; const { attachments, body, groupInvitation, preview, quote } = msg;
this.clearTypingTimers(); this.clearTypingTimers();

@ -873,7 +873,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
public async markAsDeleted() { public async markAsDeleted() {
this.set({ this.set({
isDeleted: true, isDeleted: true,
body: window.i18n('messageDeleted'), body: window.i18n('messageDeletedPlaceholder'),
quote: undefined, quote: undefined,
groupInvitation: undefined, groupInvitation: undefined,
dataExtractionNotification: undefined, dataExtractionNotification: undefined,

@ -19,6 +19,10 @@ import { perfEnd, perfStart } from '../session/utils/Performance';
import { getAllCachedECKeyPair } from './closedGroups'; import { getAllCachedECKeyPair } from './closedGroups';
import { getMessageBySenderAndTimestamp } from '../data/data'; import { getMessageBySenderAndTimestamp } from '../data/data';
import { handleCallMessage } from './callMessage'; import { handleCallMessage } from './callMessage';
import {
deleteMessageLocallyOnly,
deleteMessagesFromSwarmOnly,
} from '../interactions/conversations/unsendingInteractions';
export async function handleContentMessage(envelope: EnvelopePlus, messageHash?: string) { export async function handleContentMessage(envelope: EnvelopePlus, messageHash?: string) {
try { try {
@ -397,7 +401,7 @@ export async function innerHandleContentMessage(
); );
return; return;
} }
if (content.unsendMessage && window.lokiFeatureFlags?.useUnsendRequests) { if (content.unsendMessage) {
await handleUnsendMessage(envelope, content.unsendMessage as SignalService.Unsend); await handleUnsendMessage(envelope, content.unsendMessage as SignalService.Unsend);
} }
if (content.callMessage && window.lokiFeatureFlags?.useCallMessage) { if (content.callMessage && window.lokiFeatureFlags?.useCallMessage) {
@ -519,7 +523,20 @@ async function handleUnsendMessage(envelope: EnvelopePlus, unsendMessage: Signal
//#region executing deletion //#region executing deletion
if (messageHash && messageToDelete) { if (messageHash && messageToDelete) {
await conversation.deleteMessage(messageToDelete); const wasDeleted = await deleteMessagesFromSwarmOnly([messageToDelete]);
if (!wasDeleted) {
window.log.warn(
'handleUnsendMessage: got a request to delete ',
messageHash,
' but an error happened during deleting it from our swarm.'
);
}
// still, delete it locally
await deleteMessageLocallyOnly({
conversation,
message: messageToDelete,
deletionType: 'markDeleted',
});
} }
//#endregion //#endregion
} }

@ -18,7 +18,7 @@ import { getV2OpenGroupRoom, removeV2OpenGroupRoom } from '../../data/opengroups
import _ from 'lodash'; import _ from 'lodash';
import { getOpenGroupManager } from '../../opengroup/opengroupV2/OpenGroupManagerV2'; import { getOpenGroupManager } from '../../opengroup/opengroupV2/OpenGroupManagerV2';
import { deleteAuthToken, DeleteAuthTokenRequest } from '../../opengroup/opengroupV2/ApiAuth'; import { deleteAuthToken, DeleteAuthTokenRequest } from '../../opengroup/opengroupV2/ApiAuth';
import { deleteMessagesByConvoIdNoConfirmation } from '../../interactions/conversationInteractions'; import { deleteAllMessagesByConvoIdNoConfirmation } from '../../interactions/conversationInteractions';
let instance: ConversationController | null; let instance: ConversationController | null;
@ -228,7 +228,7 @@ export class ConversationController {
// those are the stuff to do for all contact types // those are the stuff to do for all contact types
window.log.info(`deleteContact destroyingMessages: ${id}`); window.log.info(`deleteContact destroyingMessages: ${id}`);
await deleteMessagesByConvoIdNoConfirmation(conversation.id); await deleteAllMessagesByConvoIdNoConfirmation(conversation.id);
window.log.info(`deleteContact message destroyed: ${id}`); window.log.info(`deleteContact message destroyed: ${id}`);
// 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 want to remove everything related to it, set the active_at to undefined // we just want to remove everything related to it, set the active_at to undefined

@ -68,11 +68,7 @@ let ignoreOffer = false;
let isSettingRemoteAnswerPending = false; let isSettingRemoteAnswerPending = false;
let lastOutgoingOfferTimestamp = -Infinity; let lastOutgoingOfferTimestamp = -Infinity;
const configuration = { const configuration: RTCConfiguration = {
configuration: {
offerToReceiveAudio: true,
offerToReceiveVideo: true,
},
iceServers: [ iceServers: [
{ {
urls: 'turn:freyr.getsession.org', urls: 'turn:freyr.getsession.org',
@ -80,6 +76,7 @@ const configuration = {
credential: 'webrtc', credential: 'webrtc',
}, },
], ],
iceTransportPolicy: 'relay',
}; };
let selectedCameraId: string | undefined; let selectedCameraId: string | undefined;
@ -355,6 +352,7 @@ const iceSenderDebouncer = _.debounce(async (recipient: string) => {
sdpMids: validCandidates.map(c => c.sdpMid), sdpMids: validCandidates.map(c => c.sdpMid),
sdps: validCandidates.map(c => c.candidate), sdps: validCandidates.map(c => c.candidate),
}); });
window.log.info('sending ICE CANDIDATES MESSAGE to ', recipient); window.log.info('sending ICE CANDIDATES MESSAGE to ', recipient);
await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(recipient), callIceCandicates); await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(recipient), callIceCandicates);

@ -209,6 +209,7 @@ export type PropsForMessageWithConvoProps = PropsForMessageWithoutConvoProps & {
weAreAdmin: boolean; weAreAdmin: boolean;
isSenderAdmin: boolean; isSenderAdmin: boolean;
isDeletable: boolean; isDeletable: boolean;
isDeletableForEveryone: boolean;
isBlocked: boolean; isBlocked: boolean;
isDeleted?: boolean; isDeleted?: boolean;
}; };

@ -74,6 +74,13 @@ export const getSelectedConversation = createSelector(
} }
); );
export const getSelectedConversationIsPublic = createSelector(
getSelectedConversation,
(state: ReduxConversationType | undefined): boolean => {
return state?.isPublic || false;
}
);
export const getHasIncomingCallFrom = createSelector( export const getHasIncomingCallFrom = createSelector(
getConversations, getConversations,
(state: ConversationsStateType): ReduxConversationType | undefined => { (state: ConversationsStateType): ReduxConversationType | undefined => {
@ -706,11 +713,18 @@ export const getMessagePropsByMessageId = createSelector(
const groupAdmins = (isGroup && foundMessageConversation.groupAdmins) || []; const groupAdmins = (isGroup && foundMessageConversation.groupAdmins) || [];
const weAreAdmin = groupAdmins.includes(ourPubkey) || false; const weAreAdmin = groupAdmins.includes(ourPubkey) || false;
// a message is deletable if // A message is deletable if
// either we sent it, // either we sent it,
// or the convo is not a public one (in this case, we will only be able to delete for us) // or the convo is not a public one (in this case, we will only be able to delete for us)
// or the convo is public and we are an admin // or the convo is public and we are an admin
const isDeletable = authorPhoneNumber === ourPubkey || !isPublic || (isPublic && !!weAreAdmin); const isDeletable = authorPhoneNumber === ourPubkey || !isPublic || (isPublic && !!weAreAdmin);
// A message is deletable for everyone if
// either we sent it no matter what the conversation type,
// or the convo is public and we are an admin
const isDeletableForEveryone =
authorPhoneNumber === ourPubkey || (isPublic && !!weAreAdmin) || false;
const isSenderAdmin = groupAdmins.includes(authorPhoneNumber); const isSenderAdmin = groupAdmins.includes(authorPhoneNumber);
const senderIsUs = authorPhoneNumber === ourPubkey; const senderIsUs = authorPhoneNumber === ourPubkey;
@ -726,6 +740,7 @@ export const getMessagePropsByMessageId = createSelector(
isOpenGroupV2: !!isPublic, isOpenGroupV2: !!isPublic,
isSenderAdmin, isSenderAdmin,
isDeletable, isDeletable,
isDeletableForEveryone,
weAreAdmin, weAreAdmin,
conversationType: foundMessageConversation.type, conversationType: foundMessageConversation.type,
authorPhoneNumber, authorPhoneNumber,
@ -869,6 +884,7 @@ export const getMessageContextMenuProps = createSelector(getMessagePropsByMessag
serverTimestamp, serverTimestamp,
timestamp, timestamp,
isBlocked, isBlocked,
isDeletableForEveryone,
} = props.propsForMessage; } = props.propsForMessage;
const msgProps: MessageContextMenuSelectorProps = { const msgProps: MessageContextMenuSelectorProps = {
@ -886,6 +902,7 @@ export const getMessageContextMenuProps = createSelector(getMessagePropsByMessag
serverTimestamp, serverTimestamp,
timestamp, timestamp,
isBlocked, isBlocked,
isDeletableForEveryone,
}; };
return msgProps; return msgProps;

1
ts/window.d.ts vendored

@ -47,7 +47,6 @@ declare global {
useFileOnionRequestsV2: boolean; useFileOnionRequestsV2: boolean;
padOutgoingAttachments: boolean; padOutgoingAttachments: boolean;
enablePinConversations: boolean; enablePinConversations: boolean;
useUnsendRequests: boolean;
useCallMessage: boolean; useCallMessage: boolean;
}; };
lokiSnodeAPI: LokiSnodeAPI; lokiSnodeAPI: LokiSnodeAPI;

Loading…
Cancel
Save