From f004e714f09d476f5c05e344b47637ef752ede4d Mon Sep 17 00:00:00 2001 From: Yusuf Sahin HAMZA Date: Sat, 22 Apr 2023 04:52:25 +0300 Subject: [PATCH] Add copy option to triple-dot menu of messages --- _locales/en/messages.json | 4 ++++ ts/components/conversation/Quote.stories.tsx | 2 ++ .../conversation/Timeline.stories.tsx | 2 ++ .../conversation/TimelineItem.stories.tsx | 1 + .../conversation/TimelineMessage.stories.tsx | 2 ++ .../conversation/TimelineMessage.tsx | 20 ++++++++++++++++++ ts/state/ducks/conversations.ts | 21 +++++++++++++++++++ ts/state/selectors/message.ts | 7 +++++++ ts/state/smart/TimelineItem.tsx | 2 ++ 9 files changed, 61 insertions(+) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index ae21b3d592..4f60584beb 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2352,6 +2352,10 @@ "messageformat": "More Info", "description": "Shown on the drop-down menu for an individual message, takes you to message detail screen" }, + "icu:copy": { + "messageformat": "Copy text", + "description": "Shown on the drop-down menu for an individual message, copies the message text to the clipboard" + }, "icu:MessageContextMenu__select": { "messageformat": "Select", "description": "Shown on the drop-down menu for an individual message, opens the conversation in select mode with the current message selected" diff --git a/ts/components/conversation/Quote.stories.tsx b/ts/components/conversation/Quote.stories.tsx index 498e139919..4258ebbcd7 100644 --- a/ts/components/conversation/Quote.stories.tsx +++ b/ts/components/conversation/Quote.stories.tsx @@ -83,6 +83,7 @@ const defaultMessageProps: TimelineMessagesProps = { id: 'some-id', title: 'Person X', }), + canCopy: true, canEditMessage: true, canReact: true, canReply: true, @@ -129,6 +130,7 @@ const defaultMessageProps: TimelineMessagesProps = { setMessageToEdit: action('setMessageToEdit'), setQuoteByMessageId: action('default--setQuoteByMessageId'), retryMessageSend: action('default--retryMessageSend'), + copyMessageText: action('copyMessageText'), retryDeleteForEveryone: action('default--retryDeleteForEveryone'), saveAttachment: action('saveAttachment'), scrollToQuotedMessage: action('default--scrollToQuotedMessage'), diff --git a/ts/components/conversation/Timeline.stories.tsx b/ts/components/conversation/Timeline.stories.tsx index 1d5685d42c..b015581765 100644 --- a/ts/components/conversation/Timeline.stories.tsx +++ b/ts/components/conversation/Timeline.stories.tsx @@ -47,6 +47,7 @@ function mockMessageTimelineItem( data: { id, author: getDefaultConversation({}), + canCopy: true, canDeleteForEveryone: false, canDownload: true, canEditMessage: true, @@ -282,6 +283,7 @@ const actions = () => ({ reactToMessage: action('reactToMessage'), setMessageToEdit: action('setMessageToEdit'), setQuoteByMessageId: action('setQuoteByMessageId'), + copyMessageText: action('copyMessageText'), retryDeleteForEveryone: action('retryDeleteForEveryone'), retryMessageSend: action('retryMessageSend'), saveAttachment: action('saveAttachment'), diff --git a/ts/components/conversation/TimelineItem.stories.tsx b/ts/components/conversation/TimelineItem.stories.tsx index d1a4c7df70..f3003bad12 100644 --- a/ts/components/conversation/TimelineItem.stories.tsx +++ b/ts/components/conversation/TimelineItem.stories.tsx @@ -69,6 +69,7 @@ const getDefaultProps = () => ({ clearTargetedMessage: action('clearTargetedMessage'), setMessageToEdit: action('setMessageToEdit'), setQuoteByMessageId: action('setQuoteByMessageId'), + copyMessageText: action('copyMessageText'), retryDeleteForEveryone: action('retryDeleteForEveryone'), retryMessageSend: action('retryMessageSend'), blockGroupLinkRequests: action('blockGroupLinkRequests'), diff --git a/ts/components/conversation/TimelineMessage.stories.tsx b/ts/components/conversation/TimelineMessage.stories.tsx index cbd4570427..ab5cb1023f 100644 --- a/ts/components/conversation/TimelineMessage.stories.tsx +++ b/ts/components/conversation/TimelineMessage.stories.tsx @@ -245,6 +245,7 @@ const createProps = (overrideProps: Partial = {}): Props => ({ attachments: overrideProps.attachments, author: overrideProps.author || getDefaultConversation(), bodyRanges: overrideProps.bodyRanges, + canCopy: true, canEditMessage: true, canReact: true, canReply: true, @@ -324,6 +325,7 @@ const createProps = (overrideProps: Partial = {}): Props => ({ saveAttachment: action('saveAttachment'), setQuoteByMessageId: action('setQuoteByMessageId'), retryMessageSend: action('retryMessageSend'), + copyMessageText: action('copyMessageText'), retryDeleteForEveryone: action('retryDeleteForEveryone'), scrollToQuotedMessage: action('scrollToQuotedMessage'), targetMessage: action('targetMessage'), diff --git a/ts/components/conversation/TimelineMessage.tsx b/ts/components/conversation/TimelineMessage.tsx index b5bc2ead5e..7157d408b6 100644 --- a/ts/components/conversation/TimelineMessage.tsx +++ b/ts/components/conversation/TimelineMessage.tsx @@ -32,6 +32,7 @@ import type { DeleteMessagesPropsType } from '../../state/ducks/globalModals'; export type PropsData = { canDownload: boolean; + canCopy: boolean; canEditMessage: boolean; canRetry: boolean; canRetryDeleteForEveryone: boolean; @@ -50,6 +51,7 @@ export type PropsActions = { { emoji, remove }: { emoji: string; remove: boolean } ) => void; retryMessageSend: (id: string) => void; + copyMessageText: (id: string) => void; retryDeleteForEveryone: (id: string) => void; setMessageToEdit: (conversationId: string, messageId: string) => unknown; setQuoteByMessageId: (conversationId: string, messageId: string) => void; @@ -82,6 +84,7 @@ export function TimelineMessage(props: Props): JSX.Element { attachments, author, canDownload, + canCopy, canEditMessage, canReact, canReply, @@ -101,6 +104,7 @@ export function TimelineMessage(props: Props): JSX.Element { isTapToView, kickOffAttachmentDownload, payment, + copyMessageText, pushPanelForConversation, reactToMessage, renderEmojiPicker, @@ -367,6 +371,7 @@ export function TimelineMessage(props: Props): JSX.Element { ? () => retryDeleteForEveryone(id) : undefined } + onCopy={canCopy ? () => copyMessageText(id) : undefined} onSelect={() => toggleSelectMessage(conversationId, id, false, true)} onForward={ canForward ? () => toggleForwardMessagesModal([id]) : undefined @@ -554,6 +559,7 @@ type MessageContextProps = { onReact: (() => void) | undefined; onRetryMessageSend: (() => void) | undefined; onRetryDeleteForEveryone: (() => void) | undefined; + onCopy: (() => void) | undefined; onForward: (() => void) | undefined; onDeleteMessage: () => void; onMoreInfo: () => void; @@ -569,6 +575,7 @@ const MessageContextMenu = ({ onReplyToMessage, onReact, onMoreInfo, + onCopy, onSelect, onRetryMessageSend, onRetryDeleteForEveryone, @@ -667,6 +674,19 @@ const MessageContextMenu = ({ > {i18n('icu:MessageContextMenu__select')} + {onCopy && ( + { + onCopy(); + }} + > + {i18n('icu:copy')} + + )} { + return async dispatch => { + const message = await getMessageById(messageId); + if (!message) { + throw new Error(`copy: Message ${messageId} missing!`); + } + + const body = message.getNotificationText(); + clipboard.writeText(body); + + dispatch({ + type: 'NOOP', + payload: null, + }); + }; +} + export function retryDeleteForEveryone( messageId: string ): ThunkAction { diff --git a/ts/state/selectors/message.ts b/ts/state/selectors/message.ts index 4c7c20336d..4e3c61cd36 100644 --- a/ts/state/selectors/message.ts +++ b/ts/state/selectors/message.ts @@ -720,6 +720,7 @@ export const getPropsForMessage = ( storyReplyContext, textAttachment, payment, + canCopy: canCopy(message), canEditMessage: canEditMessage(message), canDeleteForEveryone: canDeleteForEveryone(message), canDownload: canDownload(message, conversationSelector), @@ -1773,6 +1774,12 @@ export function canReact( return canReplyOrReact(message, ourConversationId, conversation); } +export function canCopy( + message: Pick +): boolean { + return !message.deletedForEveryone && Boolean(message.body); +} + export function canDeleteForEveryone( message: Pick< MessageWithUIFieldsType, diff --git a/ts/state/smart/TimelineItem.tsx b/ts/state/smart/TimelineItem.tsx index 9c70e76828..002ce3078f 100644 --- a/ts/state/smart/TimelineItem.tsx +++ b/ts/state/smart/TimelineItem.tsx @@ -118,6 +118,7 @@ export function SmartTimelineItem(props: ExternalProps): JSX.Element { messageExpanded, openGiftBadge, pushPanelForConversation, + copyMessageText, retryDeleteForEveryone, retryMessageSend, saveAttachment, @@ -184,6 +185,7 @@ export function SmartTimelineItem(props: ExternalProps): JSX.Element { openGiftBadge={openGiftBadge} pushPanelForConversation={pushPanelForConversation} reactToMessage={reactToMessage} + copyMessageText={copyMessageText} retryDeleteForEveryone={retryDeleteForEveryone} retryMessageSend={retryMessageSend} returnToActiveCall={returnToActiveCall}