diff --git a/lib/utils/delete-message-utils.js b/lib/utils/delete-message-utils.js --- a/lib/utils/delete-message-utils.js +++ b/lib/utils/delete-message-utils.js @@ -15,8 +15,12 @@ type OutboundDMOperationSpecification, } from '../shared/dm-ops/dm-op-utils.js'; import { useProcessAndSendDMOperation } from '../shared/dm-ops/process-dm-ops.js'; +import { useThreadHasPermission } from '../shared/thread-utils.js'; import type { DMSendDeleteMessageOperation } from '../types/dm-ops.js'; import type { MessageInfo } from '../types/message-types.js'; +import { isComposableMessageType } from '../types/message-types.js'; +import type { ThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; +import { threadPermissions } from '../types/thread-permission-types.js'; import { thickThreadTypes } from '../types/thread-types-enum.js'; function useDeleteMessage(): (message: MessageInfo) => Promise { @@ -74,4 +78,39 @@ ); } -export { useDeleteMessage }; +function useCanDeleteMessage( + threadInfo: ThreadInfo, + targetMessageInfo: ?MessageInfo, +): boolean { + const currentUserInfo = useSelector(state => state.currentUserInfo); + const canDeleteOwnMessages = useThreadHasPermission( + threadInfo, + threadPermissions.DELETE_OWN_MESSAGES, + ); + const canDeleteAllMessages = useThreadHasPermission( + threadInfo, + threadPermissions.DELETE_ALL_MESSAGES, + ); + + if ( + !targetMessageInfo || + !targetMessageInfo.id || + !isComposableMessageType(targetMessageInfo.type) + ) { + return false; + } + + if (!currentUserInfo || !currentUserInfo.id) { + return false; + } + + if (canDeleteAllMessages) { + return true; + } + + return ( + currentUserInfo.id === targetMessageInfo.creator.id && canDeleteOwnMessages + ); +} + +export { useDeleteMessage, useCanDeleteMessage }; diff --git a/native/chat/multimedia-message-tooltip-modal.react.js b/native/chat/multimedia-message-tooltip-modal.react.js --- a/native/chat/multimedia-message-tooltip-modal.react.js +++ b/native/chat/multimedia-message-tooltip-modal.react.js @@ -2,6 +2,8 @@ import * as React from 'react'; +import { useDeleteMessage } from 'lib/utils/delete-message-utils.js'; + import { useOnPressReport } from './message-report-utils.js'; import MultimediaMessageTooltipButton from './multimedia-message-tooltip-button.react.js'; import { useAnimatedNavigateToSidebar } from './sidebar-navigation.js'; @@ -27,7 +29,7 @@ function TooltipMenu( props: TooltipMenuProps<'MultimediaMessageTooltipModal'>, ): React.Node { - const { route, tooltipItem: TooltipItem } = props; + const { route, tooltipItem: TooltipItem, navigation } = props; const overlayContext = React.useContext(OverlayContext); @@ -62,6 +64,18 @@ [], ); + const deleteMessage = useDeleteMessage(); + const onPressDelete = React.useCallback(async () => { + await deleteMessage(route.params.item.messageInfo); + navigation.goBackOnce(); + }, [deleteMessage, navigation, route.params.item.messageInfo]); + const renderDeleteIcon = React.useCallback( + (style: TextStyle) => ( + + ), + [], + ); + return ( <> + ); } diff --git a/native/chat/multimedia-message.react.js b/native/chat/multimedia-message.react.js --- a/native/chat/multimedia-message.react.js +++ b/native/chat/multimedia-message.react.js @@ -13,7 +13,9 @@ import { useCanCreateReactionFromMessage } from 'lib/shared/reaction-utils.js'; import { useCanCreateSidebarFromMessage } from 'lib/shared/sidebar-utils.js'; import type { MediaInfo } from 'lib/types/media-types.js'; +import { useCanDeleteMessage } from 'lib/utils/delete-message-utils.js'; +import { ChatContext, type ChatContextType } from './chat-context.js'; import ComposedMessage from './composed-message.react.js'; import { InnerMultimediaMessage } from './inner-multimedia-message.react.js'; import { @@ -21,7 +23,6 @@ multimediaMessageSendFailed, } from './multimedia-message-utils.js'; import { getMessageTooltipKey } from './utils.js'; -import { ChatContext, type ChatContextType } from '../chat/chat-context.js'; import { OverlayContext } from '../navigation/overlay-context.js'; import type { OverlayContextType } from '../navigation/overlay-context.js'; import { @@ -53,6 +54,7 @@ +chatContext: ?ChatContextType, +canCreateSidebarFromMessage: boolean, +canCreateReactionFromMessage: boolean, + +canDeleteMessage: boolean, }; type State = { +clickable: boolean, @@ -106,6 +108,10 @@ result.push('report'); } + if (this.props.canDeleteMessage) { + result.push('delete'); + } + return result; } @@ -208,6 +214,7 @@ canCreateSidebarFromMessage, canTogglePins, canCreateReactionFromMessage, + canDeleteMessage, ...viewProps } = this.props; return ( @@ -248,6 +255,10 @@ props.item.threadInfo, props.item.messageInfo, ); + const canDeleteMessage = useCanDeleteMessage( + props.item.threadInfo, + props.item.messageInfo, + ); return ( ); }); diff --git a/native/chat/text-message-tooltip-modal.react.js b/native/chat/text-message-tooltip-modal.react.js --- a/native/chat/text-message-tooltip-modal.react.js +++ b/native/chat/text-message-tooltip-modal.react.js @@ -5,6 +5,7 @@ import * as React from 'react'; import { createMessageReply } from 'lib/shared/message-utils.js'; +import { useDeleteMessage } from 'lib/utils/delete-message-utils.js'; import { MessageEditingContext } from './message-editing-context.react.js'; import { useNavigateToThread } from './message-list-types.js'; @@ -36,7 +37,7 @@ function TooltipMenu( props: TooltipMenuProps<'TextMessageTooltipModal'>, ): React.Node { - const { route, tooltipItem: TooltipItem } = props; + const { route, tooltipItem: TooltipItem, navigation } = props; const { threadInfo } = route.params.item; const overlayContext = React.useContext(OverlayContext); @@ -135,6 +136,18 @@ [], ); + const deleteMessage = useDeleteMessage(); + const onPressDelete = React.useCallback(async () => { + await deleteMessage(messageInfo); + navigation.goBackOnce(); + }, [deleteMessage, messageInfo, navigation]); + const renderDeleteIcon = React.useCallback( + (style: TextStyle) => ( + + ), + [], + ); + return ( <> + ); } diff --git a/native/chat/text-message.react.js b/native/chat/text-message.react.js --- a/native/chat/text-message.react.js +++ b/native/chat/text-message.react.js @@ -8,6 +8,7 @@ import { useCanCreateSidebarFromMessage } from 'lib/shared/sidebar-utils.js'; import { useThreadHasPermission } from 'lib/shared/thread-utils.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; +import { useCanDeleteMessage } from 'lib/utils/delete-message-utils.js'; import type { ChatNavigationProp } from './chat.react.js'; import ComposedMessage from './composed-message.react.js'; @@ -65,6 +66,7 @@ +isUserProfileBottomSheetActive: boolean, +canEditMessage: boolean, +currentUserIsVoiced: boolean, + +canDeleteMessage: boolean, }; class TextMessage extends React.PureComponent { message: ?React.ElementRef; @@ -94,6 +96,7 @@ canEditMessage, canTogglePins, currentUserIsVoiced, + canDeleteMessage, ...viewProps } = this.props; @@ -173,6 +176,10 @@ result.push('report'); } + if (this.props.canDeleteMessage) { + result.push('delete'); + } + return result; } @@ -292,6 +299,11 @@ threadPermissions.VOICED, ); + const canDeleteMessage = useCanDeleteMessage( + props.item.threadInfo, + props.item.messageInfo, + ); + return ( ); }); diff --git a/web/tooltips/tooltip-action-utils.js b/web/tooltips/tooltip-action-utils.js --- a/web/tooltips/tooltip-action-utils.js +++ b/web/tooltips/tooltip-action-utils.js @@ -1,5 +1,7 @@ // @flow +import { faTrash } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import invariant from 'invariant'; import * as React from 'react'; @@ -22,6 +24,10 @@ import { messageTypes } from 'lib/types/message-types-enum.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; +import { + useCanDeleteMessage, + useDeleteMessage, +} from 'lib/utils/delete-message-utils.js'; import { useCanToggleMessagePin } from 'lib/utils/message-pinning-utils.js'; import LabelTooltip from './label-toolitp.react.js'; @@ -381,6 +387,35 @@ ]); } +function useMessageDeleteAction( + item: ChatMessageInfoItem, + threadInfo: ThreadInfo, +): ?MessageTooltipAction { + const { messageInfo } = item; + + const canDeleteMessage = useCanDeleteMessage(threadInfo, messageInfo); + const deleteMessage = useDeleteMessage(); + const { clearTooltip } = useTooltipContext(); + + return React.useMemo(() => { + if (!canDeleteMessage) { + return null; + } + const buttonContent = ; + const onClickDelete = async () => { + if (messageInfo) { + await deleteMessage(messageInfo); + } + clearTooltip(); + }; + return { + actionButtonContent: buttonContent, + onClick: onClickDelete, + label: 'Delete', + }; + }, [canDeleteMessage, clearTooltip, deleteMessage, messageInfo]); +} + function useMessageTooltipActions( item: ChatMessageInfoItem, threadInfo: ThreadInfo, @@ -391,6 +426,7 @@ const reactAction = useMessageReactAction(item, threadInfo); const togglePinAction = useMessageTogglePinAction(item, threadInfo); const editAction = useMessageEditAction(item, threadInfo); + const deleteAction = useMessageDeleteAction(item, threadInfo); return React.useMemo( () => [ @@ -400,6 +436,7 @@ reactAction, togglePinAction, editAction, + deleteAction, ].filter(Boolean), [ replyAction, @@ -408,6 +445,7 @@ reactAction, togglePinAction, editAction, + deleteAction, ], ); }