From 0f2fcbb6e38005a884f931c70792b34a208d0652 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 25 Oct 2021 17:04:51 +1100 Subject: [PATCH] delete for opengroups is working --- _locales/en/messages.json | 23 +- stylesheets/_session_conversation.scss | 2 +- ts/components/ConversationListItem.tsx | 6 +- .../conversation/ConversationHeader.tsx | 62 ++- ts/components/conversation/MessageDetail.tsx | 8 +- .../message/MessageContentWithStatus.tsx | 21 +- .../message/MessageContextMenu.tsx | 35 +- .../conversation/SessionRightPanel.tsx | 4 +- .../session/menu/ConversationHeaderMenu.tsx | 2 +- .../menu/ConversationListItemContextMenu.tsx | 2 +- ts/components/session/menu/Menu.tsx | 32 +- ts/interactions/conversationInteractions.ts | 265 +------------ .../conversations/unsendingInteractions.ts | 366 ++++++++++++++++++ ts/models/conversation.ts | 124 +----- ts/models/message.ts | 2 +- ts/receiver/contentMessage.ts | 21 +- .../conversations/ConversationController.ts | 4 +- ts/session/utils/CallManager.ts | 8 +- ts/state/ducks/conversations.ts | 1 + ts/state/selectors/conversations.ts | 19 +- ts/window.d.ts | 1 - 21 files changed, 518 insertions(+), 490 deletions(-) create mode 100644 ts/interactions/conversations/unsendingInteractions.ts diff --git a/_locales/en/messages.json b/_locales/en/messages.json index d5bd06d78..f80e651b9 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -110,13 +110,14 @@ "continue": "Continue", "error": "Error", "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 don’t 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", + "messageDeletedPlaceholder": "This message has been deleted", "from": "From:", "to": "To:", "sent": "Sent", @@ -125,14 +126,6 @@ "groupMembers": "Group members", "moreInformation": "More information", "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?", "clearAllData": "Clear All Data", "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.", "recoveryPhraseRevealButtonText": "Reveal Recovery Phrase", "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", "goToOurSurvey": "Go to our survey", "incomingCall": "Incoming call", diff --git a/stylesheets/_session_conversation.scss b/stylesheets/_session_conversation.scss index 141fc5b95..c920c44b4 100644 --- a/stylesheets/_session_conversation.scss +++ b/stylesheets/_session_conversation.scss @@ -78,7 +78,7 @@ } .message-selection-overlay .button-group { - float: right; + display: flex; } } diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index e94606cef..58c7401ae 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -86,7 +86,7 @@ const HeaderItem = (props: { const pinIcon = isMessagesSection && isPinned ? ( - + ) : null; const NotificationSettingIcon = () => { @@ -99,11 +99,11 @@ const HeaderItem = (props: { return null; case 'disabled': return ( - + ); case 'mentions_only': return ( - + ); default: return null; diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index d8ec8287e..2b0a64f56 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -15,6 +15,8 @@ import { getConversationHeaderTitleProps, getCurrentNotificationSettingText, getSelectedConversation, + getSelectedConversationIsPublic, + getSelectedConversationKey, getSelectedMessageIds, isMessageDetailView, isMessageSelectionMode, @@ -23,7 +25,10 @@ import { import { useDispatch, useSelector } from 'react-redux'; import { useMembersAvatars } from '../../hooks/useMembersAvatar'; -import { deleteMessagesById } from '../../interactions/conversationInteractions'; +import { + deleteMessagesById, + deleteMessagesByIdForEveryone, +} from '../../interactions/conversations/unsendingInteractions'; import { closeMessageDetailsView, closeRightPanel, @@ -67,16 +72,32 @@ export type ConversationHeaderProps = { left: boolean; }; -const SelectionOverlay = (props: { - onDeleteSelectedMessages: () => void; - onCloseOverlay: () => void; - isPublic: boolean; -}) => { - const { onDeleteSelectedMessages, onCloseOverlay, isPublic } = props; +const SelectionOverlay = () => { + const selectedMessageIds = useSelector(getSelectedMessageIds); + const selectedConversationKey = useSelector(getSelectedConversationKey); + const isPublic = useSelector(getSelectedConversationIsPublic); + const dispatch = useDispatch(); + const { i18n } = window; - const isServerDeletable = isPublic; - const deleteMessageButtonText = i18n(isServerDeletable ? 'deleteForEveryone' : 'delete'); + function onCloseOverlay() { + 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 (
@@ -85,11 +106,19 @@ const SelectionOverlay = (props: {
+ {!isOnlyServerDeletable && ( + + )}
@@ -285,7 +314,6 @@ export const ConversationHeaderWithDetails = () => { const headerProps = useSelector(getConversationHeaderProps); const isSelectionMode = useSelector(isMessageSelectionMode); - const selectedMessageIds = useSelector(getSelectedMessageIds); const selectedConversation = useSelector(getSelectedConversation); const memberDetails = useMembersAvatars(selectedConversation); const isMessageDetailOpened = useSelector(isMessageDetailView); @@ -366,15 +394,7 @@ export const ConversationHeaderWithDetails = () => { /> - {isSelectionMode && ( - dispatch(resetSelectedMessageIds())} - onDeleteSelectedMessages={() => { - void deleteMessagesById(selectedMessageIds, conversationKey, true); - }} - /> - )} + {isSelectionMode && } ); }; diff --git a/ts/components/conversation/MessageDetail.tsx b/ts/components/conversation/MessageDetail.tsx index 183c4208c..15e94b201 100644 --- a/ts/components/conversation/MessageDetail.tsx +++ b/ts/components/conversation/MessageDetail.tsx @@ -5,13 +5,13 @@ import moment from 'moment'; import { Avatar, AvatarSize } from '../Avatar'; import { ContactName } from './ContactName'; import { Message } from './Message'; -import { deleteMessagesById } from '../../interactions/conversationInteractions'; import { useSelector } from 'react-redux'; import { ContactPropsMessageDetail } from '../../state/ducks/conversations'; import { getMessageDetailsViewProps, getMessageIsDeletable, } from '../../state/selectors/conversations'; +import { deleteMessagesById } from '../../interactions/conversations/unsendingInteractions'; const AvatarItem = (props: { contact: ContactPropsMessageDetail }) => { const { avatarPath, pubkey, name, profileName } = props.contact; @@ -26,12 +26,12 @@ const DeleteButtonItem = (props: { messageId: string; convoId: string; isDeletab return props.isDeletable ? (
) : null; diff --git a/ts/components/conversation/message/MessageContentWithStatus.tsx b/ts/components/conversation/message/MessageContentWithStatus.tsx index 4a15127f4..ac9896112 100644 --- a/ts/components/conversation/message/MessageContentWithStatus.tsx +++ b/ts/components/conversation/message/MessageContentWithStatus.tsx @@ -35,24 +35,9 @@ export const MessageContentWithStatuses = (props: Props) => { const onClickOnMessageOuterContainer = useCallback( (event: React.MouseEvent) => { - const selection = window.getSelection(); - // Text is being selected - if (selection && selection.type === 'Range') { - 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) { + if (multiSelectMode && messageId) { + event.preventDefault(); + event.stopPropagation(); dispatch(toggleSelectedMessageId(messageId)); } }, diff --git a/ts/components/conversation/message/MessageContextMenu.tsx b/ts/components/conversation/message/MessageContextMenu.tsx index b4d8c2bc5..f86de1ea8 100644 --- a/ts/components/conversation/message/MessageContextMenu.tsx +++ b/ts/components/conversation/message/MessageContextMenu.tsx @@ -4,7 +4,7 @@ import { animation, Item, Menu } from 'react-contexify'; import { MessageInteraction } from '../../../interactions'; import { getMessageById } from '../../../data/data'; -import { deleteMessagesById, replyToMessage } from '../../../interactions/conversationInteractions'; +import { replyToMessage } from '../../../interactions/conversationInteractions'; import { showMessageDetailsView, toggleSelectedMessageId, @@ -16,8 +16,12 @@ import { } from '../../../interactions/messageInteractions'; import { MessageRenderingProps } from '../../../models/messageType'; import { pushUnblockToSend } from '../../../session/utils/Toast'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { getMessageContextMenuProps } from '../../../state/selectors/conversations'; +import { + deleteMessagesById, + deleteMessagesByIdForEveryone, +} from '../../../interactions/conversations/unsendingInteractions'; export type MessageContextMenuSelectorProps = Pick< MessageRenderingProps, @@ -35,6 +39,7 @@ export type MessageContextMenuSelectorProps = Pick< | 'serverTimestamp' | 'timestamp' | 'isBlocked' + | 'isDeletableForEveryone' >; type Props = { messageId: string; contextMenuId: string }; @@ -42,6 +47,7 @@ type Props = { messageId: string; contextMenuId: string }; // tslint:disable: max-func-body-length cyclomatic-complexity export const MessageContextMenu = (props: Props) => { const selected = useSelector(state => getMessageContextMenuProps(state as any, props.messageId)); + const dispatch = useDispatch(); if (!selected) { return null; @@ -53,6 +59,7 @@ export const MessageContextMenu = (props: Props) => { direction, status, isDeletable, + isDeletableForEveryone, isPublic, isOpenGroupV2, weAreAdmin, @@ -85,14 +92,15 @@ export const MessageContextMenu = (props: Props) => { const found = await getMessageById(messageId); if (found) { const messageDetailsProps = await found.getPropsForMessageDetail(); - window.inboxStore?.dispatch(showMessageDetailsView(messageDetailsProps)); + dispatch(showMessageDetailsView(messageDetailsProps)); } else { window.log.warn(`Message ${messageId} not found in db`); } }; const selectMessageText = window.i18n('selectMessage'); - const deleteMessageText = window.i18n('deleteMessage'); + const deleteMessageJustForMeText = window.i18n('deleteJustForMe'); + const unsendMessageText = window.i18n('deleteForEveryone'); const addModerator = useCallback(() => { void addSenderAsModerator(authorPhoneNumber, convoId); @@ -151,11 +159,15 @@ export const MessageContextMenu = (props: Props) => { }, [authorPhoneNumber, convoId]); const onSelect = useCallback(() => { - window.inboxStore?.dispatch(toggleSelectedMessageId(messageId)); + dispatch(toggleSelectedMessageId(messageId)); }, [messageId]); const onDelete = useCallback(() => { - void deleteMessagesById([messageId], convoId, true); + void deleteMessagesById([messageId], convoId); + }, [convoId, messageId]); + + const onDeleteForEveryone = useCallback(() => { + void deleteMessagesByIdForEveryone([messageId], convoId); }, [convoId, messageId]); return ( @@ -176,7 +188,16 @@ export const MessageContextMenu = (props: Props) => { {isDeletable ? ( <> {selectMessageText} - {deleteMessageText} + + ) : null} + {isDeletable && !isPublic ? ( + <> + {deleteMessageJustForMeText} + + ) : null} + {isDeletableForEveryone ? ( + <> + {unsendMessageText} ) : null} {weAreAdmin && isPublic ? {window.i18n('banUser')} : null} diff --git a/ts/components/session/conversation/SessionRightPanel.tsx b/ts/components/session/conversation/SessionRightPanel.tsx index 6fee96189..5d236d766 100644 --- a/ts/components/session/conversation/SessionRightPanel.tsx +++ b/ts/components/session/conversation/SessionRightPanel.tsx @@ -13,7 +13,7 @@ import { } from '../../../data/data'; import { SpacerLG } from '../../basic/Text'; import { - deleteMessagesByConvoIdWithConfirmation, + deleteAllMessagesByConvoIdWithConfirmation, setDisappearingMessagesByConvoId, showAddModeratorsByConvoId, showInviteContactByConvoId, @@ -249,7 +249,7 @@ export const SessionRightPanelWithDetails = () => { const deleteConvoAction = isPublic ? () => { - deleteMessagesByConvoIdWithConfirmation(id); + deleteAllMessagesByConvoIdWithConfirmation(id); } : () => { showLeaveGroupByConvoId(id); diff --git a/ts/components/session/menu/ConversationHeaderMenu.tsx b/ts/components/session/menu/ConversationHeaderMenu.tsx index 43aea0ae4..248ad78ca 100644 --- a/ts/components/session/menu/ConversationHeaderMenu.tsx +++ b/ts/components/session/menu/ConversationHeaderMenu.tsx @@ -78,7 +78,7 @@ const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => { {getMarkAllReadMenuItem(conversationId)} {getChangeNicknameMenuItem(isMe, isGroup, conversationId)} {getClearNicknameMenuItem(isMe, hasNickname, isGroup, conversationId)} - {getDeleteMessagesMenuItem(isPublic, conversationId)} + {getDeleteMessagesMenuItem(conversationId)} {getAddModeratorsMenuItem(weAreAdmin, isPublic, isKickedFromGroup, conversationId)} {getRemoveModeratorsMenuItem(weAreAdmin, isPublic, isKickedFromGroup, conversationId)} {getUpdateGroupNameMenuItem(weAreAdmin, isKickedFromGroup, left, conversationId)} diff --git a/ts/components/session/menu/ConversationListItemContextMenu.tsx b/ts/components/session/menu/ConversationListItemContextMenu.tsx index 0dc4d65bd..da9953af4 100644 --- a/ts/components/session/menu/ConversationListItemContextMenu.tsx +++ b/ts/components/session/menu/ConversationListItemContextMenu.tsx @@ -76,7 +76,7 @@ const ConversationListItemContextMenu = (props: PropsContextConversationItem) => {getMarkAllReadMenuItem(conversationId)} {getChangeNicknameMenuItem(isMe, isGroup, conversationId)} {getClearNicknameMenuItem(isMe, hasNickname, isGroup, conversationId)} - {getDeleteMessagesMenuItem(isPublic, conversationId)} + {getDeleteMessagesMenuItem(conversationId)} {getInviteContactMenuItem(isGroup, isPublic, conversationId)} {getDeleteContactMenuItem(isGroup, isPublic, left, isKickedFromGroup, conversationId)} {getLeaveGroupMenuItem(isKickedFromGroup, left, isGroup, isPublic, conversationId)} diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index b7fb86cd1..17f5606d5 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -23,7 +23,7 @@ import { blockConvoById, clearNickNameByConvoId, copyPublicKeyByConvoId, - deleteMessagesByConvoIdWithConfirmation, + deleteAllMessagesByConvoIdWithConfirmation, markAllReadByConvoId, setDisappearingMessagesByConvoId, setNotificationForConvoId, @@ -70,10 +70,6 @@ function showChangeNickname(isMe: boolean, isGroup: boolean) { return !isMe && !isGroup; } -function showDeleteMessages(isPublic: boolean): boolean { - return !isPublic; -} - // we want to show the copyId for open groups and private chats only function showCopyId(isPublic: boolean, isGroup: boolean): boolean { return !isGroup || isPublic; @@ -546,20 +542,16 @@ export function getChangeNicknameMenuItem( return null; } -export function getDeleteMessagesMenuItem( - isPublic: boolean | undefined, - conversationId: string -): JSX.Element | null { - if (showDeleteMessages(Boolean(isPublic))) { - return ( - { - deleteMessagesByConvoIdWithConfirmation(conversationId); - }} - > - {window.i18n('deleteMessages')} - - ); - } +export function getDeleteMessagesMenuItem(conversationId: string): JSX.Element | null { + return ( + { + deleteAllMessagesByConvoIdWithConfirmation(conversationId); + }} + > + {window.i18n('deleteMessages')} + + ); + return null; } diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index 3160ed6db..1ce06cd3e 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -5,13 +5,7 @@ import { } from '../opengroup/utils/OpenGroupUtils'; import { getV2OpenGroupRoom } from '../data/opengroups'; import { SyncUtils, ToastUtils, UserUtils } from '../session/utils'; -import { - ConversationModel, - ConversationNotificationSettingType, - ConversationTypeEnum, -} from '../models/conversation'; -import { MessageModel } from '../models/message'; -import { ApiV2 } from '../opengroup/opengroupV2'; +import { ConversationNotificationSettingType, ConversationTypeEnum } from '../models/conversation'; import _ from 'lodash'; import { getConversationController } from '../session/conversations'; @@ -32,11 +26,7 @@ import { lastAvatarUploadTimestamp, removeAllMessagesInConversation, } from '../data/data'; -import { - conversationReset, - quoteMessage, - resetSelectedMessageIds, -} from '../state/ducks/conversations'; +import { conversationReset, quoteMessage } from '../state/ducks/conversations'; import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsManager'; import { IMAGE_JPEG } from '../types/MIME'; import { FSv2 } from '../fileserver'; @@ -86,61 +76,6 @@ export async function copyPublicKeyByConvoId(convoId: string) { 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, - convo: ConversationModel -): Promise> { - 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) { const conversation = getConversationController().get(conversationId); @@ -286,7 +221,7 @@ export function showChangeNickNameByConvoId(conversationId: string) { window.inboxStore?.dispatch(changeNickNameModal({ conversationId })); } -export async function deleteMessagesByConvoIdNoConfirmation(conversationId: string) { +export async function deleteAllMessagesByConvoIdNoConfirmation(conversationId: string) { const conversation = getConversationController().get(conversationId); await removeAllMessagesInConversation(conversationId); window.inboxStore?.dispatch(conversationReset(conversationId)); @@ -302,13 +237,13 @@ export async function deleteMessagesByConvoIdNoConfirmation(conversationId: stri await conversation.commit(); } -export function deleteMessagesByConvoIdWithConfirmation(conversationId: string) { +export function deleteAllMessagesByConvoIdWithConfirmation(conversationId: string) { const onClickClose = () => { window?.inboxStore?.dispatch(updateConfirmModal(null)); }; const onClickOk = async () => { - await deleteMessagesByConvoIdNoConfirmation(conversationId); + await deleteAllMessagesByConvoIdNoConfirmation(conversationId); 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) { - 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 -) { - 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, - conversation: ConversationModel, - deleteForEveryone: boolean = true -) => { - let toDeleteLocallyIds: Array; - - 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; - 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, - 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) { const quotedMessageModel = await getMessageById(messageId); if (!quotedMessageModel) { diff --git a/ts/interactions/conversations/unsendingInteractions.ts b/ts/interactions/conversations/unsendingInteractions.ts new file mode 100644 index 000000000..414e2a7c6 --- /dev/null +++ b/ts/interactions/conversations/unsendingInteractions.ts @@ -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 +) { + 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) { + //#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) { + 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 +) { + 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 +) { + 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, + 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, + 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, + 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, 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, + convo: ConversationModel +): Promise> { + 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 []; + } +} diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 8eee66971..b48ba361d 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -48,8 +48,7 @@ import { import { ed25519Str } from '../session/onions/onionPath'; import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsManager'; import { IMAGE_JPEG } from '../types/MIME'; -import { UnsendMessage } from '../session/messages/outgoing/controlMessage/UnsendMessage'; -import { getLatestTimestampOffset, networkDeleteMessages } from '../session/snode_api/SNodeAPI'; +import { getLatestTimestampOffset } from '../session/snode_api/SNodeAPI'; export enum ConversationTypeEnum { GROUP = 'group', @@ -800,127 +799,6 @@ export class ConversationModel extends Backbone.Model { } } - /** - * @param messages Messages to delete - */ - public async deleteMessages(messages: Array) { - 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 { - //#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) { - 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 { - 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) { const { attachments, body, groupInvitation, preview, quote } = msg; this.clearTypingTimers(); diff --git a/ts/models/message.ts b/ts/models/message.ts index ce61f6b59..43b5a7f07 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -873,7 +873,7 @@ export class MessageModel extends Backbone.Model { public async markAsDeleted() { this.set({ isDeleted: true, - body: window.i18n('messageDeleted'), + body: window.i18n('messageDeletedPlaceholder'), quote: undefined, groupInvitation: undefined, dataExtractionNotification: undefined, diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index c3f900f33..73ce9ae2b 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -19,6 +19,10 @@ import { perfEnd, perfStart } from '../session/utils/Performance'; import { getAllCachedECKeyPair } from './closedGroups'; import { getMessageBySenderAndTimestamp } from '../data/data'; import { handleCallMessage } from './callMessage'; +import { + deleteMessageLocallyOnly, + deleteMessagesFromSwarmOnly, +} from '../interactions/conversations/unsendingInteractions'; export async function handleContentMessage(envelope: EnvelopePlus, messageHash?: string) { try { @@ -397,7 +401,7 @@ export async function innerHandleContentMessage( ); return; } - if (content.unsendMessage && window.lokiFeatureFlags?.useUnsendRequests) { + if (content.unsendMessage) { await handleUnsendMessage(envelope, content.unsendMessage as SignalService.Unsend); } if (content.callMessage && window.lokiFeatureFlags?.useCallMessage) { @@ -519,7 +523,20 @@ async function handleUnsendMessage(envelope: EnvelopePlus, unsendMessage: Signal //#region executing deletion 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 } diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts index 08f3e39a0..6aacc13f3 100644 --- a/ts/session/conversations/ConversationController.ts +++ b/ts/session/conversations/ConversationController.ts @@ -18,7 +18,7 @@ import { getV2OpenGroupRoom, removeV2OpenGroupRoom } from '../../data/opengroups import _ from 'lodash'; import { getOpenGroupManager } from '../../opengroup/opengroupV2/OpenGroupManagerV2'; import { deleteAuthToken, DeleteAuthTokenRequest } from '../../opengroup/opengroupV2/ApiAuth'; -import { deleteMessagesByConvoIdNoConfirmation } from '../../interactions/conversationInteractions'; +import { deleteAllMessagesByConvoIdNoConfirmation } from '../../interactions/conversationInteractions'; let instance: ConversationController | null; @@ -228,7 +228,7 @@ export class ConversationController { // those are the stuff to do for all contact types window.log.info(`deleteContact destroyingMessages: ${id}`); - await deleteMessagesByConvoIdNoConfirmation(conversation.id); + await deleteAllMessagesByConvoIdNoConfirmation(conversation.id); window.log.info(`deleteContact message destroyed: ${id}`); // 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 diff --git a/ts/session/utils/CallManager.ts b/ts/session/utils/CallManager.ts index 09e8012d9..2a13e5ea4 100644 --- a/ts/session/utils/CallManager.ts +++ b/ts/session/utils/CallManager.ts @@ -68,11 +68,7 @@ let ignoreOffer = false; let isSettingRemoteAnswerPending = false; let lastOutgoingOfferTimestamp = -Infinity; -const configuration = { - configuration: { - offerToReceiveAudio: true, - offerToReceiveVideo: true, - }, +const configuration: RTCConfiguration = { iceServers: [ { urls: 'turn:freyr.getsession.org', @@ -80,6 +76,7 @@ const configuration = { credential: 'webrtc', }, ], + iceTransportPolicy: 'relay', }; let selectedCameraId: string | undefined; @@ -355,6 +352,7 @@ const iceSenderDebouncer = _.debounce(async (recipient: string) => { sdpMids: validCandidates.map(c => c.sdpMid), sdps: validCandidates.map(c => c.candidate), }); + window.log.info('sending ICE CANDIDATES MESSAGE to ', recipient); await getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(recipient), callIceCandicates); diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index b4492c517..9143a3349 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -209,6 +209,7 @@ export type PropsForMessageWithConvoProps = PropsForMessageWithoutConvoProps & { weAreAdmin: boolean; isSenderAdmin: boolean; isDeletable: boolean; + isDeletableForEveryone: boolean; isBlocked: boolean; isDeleted?: boolean; }; diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index dd6694d2e..4b5614aa7 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -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( getConversations, (state: ConversationsStateType): ReduxConversationType | undefined => { @@ -706,11 +713,18 @@ export const getMessagePropsByMessageId = createSelector( const groupAdmins = (isGroup && foundMessageConversation.groupAdmins) || []; const weAreAdmin = groupAdmins.includes(ourPubkey) || false; - // a message is deletable if + // A message is deletable if // 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 public and we are an admin 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 senderIsUs = authorPhoneNumber === ourPubkey; @@ -726,6 +740,7 @@ export const getMessagePropsByMessageId = createSelector( isOpenGroupV2: !!isPublic, isSenderAdmin, isDeletable, + isDeletableForEveryone, weAreAdmin, conversationType: foundMessageConversation.type, authorPhoneNumber, @@ -869,6 +884,7 @@ export const getMessageContextMenuProps = createSelector(getMessagePropsByMessag serverTimestamp, timestamp, isBlocked, + isDeletableForEveryone, } = props.propsForMessage; const msgProps: MessageContextMenuSelectorProps = { @@ -886,6 +902,7 @@ export const getMessageContextMenuProps = createSelector(getMessagePropsByMessag serverTimestamp, timestamp, isBlocked, + isDeletableForEveryone, }; return msgProps; diff --git a/ts/window.d.ts b/ts/window.d.ts index 7fb91f69e..2db947819 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -47,7 +47,6 @@ declare global { useFileOnionRequestsV2: boolean; padOutgoingAttachments: boolean; enablePinConversations: boolean; - useUnsendRequests: boolean; useCallMessage: boolean; }; lokiSnodeAPI: LokiSnodeAPI;