Add copy option to triple-dot menu of messages

This commit is contained in:
Yusuf Sahin HAMZA 2023-04-22 04:52:25 +03:00 committed by Josh Perez
parent f1624705a7
commit f004e714f0
9 changed files with 61 additions and 0 deletions

View file

@ -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"

View file

@ -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'),

View file

@ -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'),

View file

@ -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'),

View file

@ -245,6 +245,7 @@ const createProps = (overrideProps: Partial<Props> = {}): 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> = {}): Props => ({
saveAttachment: action('saveAttachment'),
setQuoteByMessageId: action('setQuoteByMessageId'),
retryMessageSend: action('retryMessageSend'),
copyMessageText: action('copyMessageText'),
retryDeleteForEveryone: action('retryDeleteForEveryone'),
scrollToQuotedMessage: action('scrollToQuotedMessage'),
targetMessage: action('targetMessage'),

View file

@ -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')}
</MenuItem>
{onCopy && (
<MenuItem
attributes={{
className:
'module-message__context--icon module-message__context__copy-timestamp',
}}
onClick={() => {
onCopy();
}}
>
{i18n('icu:copy')}
</MenuItem>
)}
<MenuItem
attributes={{
className:

View file

@ -12,6 +12,7 @@ import {
without,
} from 'lodash';
import { clipboard } from 'electron';
import type { ReadonlyDeep } from 'type-fest';
import type { AttachmentType } from '../../types/Attachment';
import type { StateType as RootStateType } from '../reducer';
@ -1048,6 +1049,7 @@ export const actions = {
repairOldestMessage,
replaceAvatar,
resetAllChatColors,
copyMessageText,
retryDeleteForEveryone,
retryMessageSend,
reviewGroupMemberNameCollision,
@ -2170,6 +2172,25 @@ function retryMessageSend(
};
}
export function copyMessageText(
messageId: string
): ThunkAction<void, RootStateType, unknown, NoopActionType> {
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<void, RootStateType, unknown, NoopActionType> {

View file

@ -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<MessageWithUIFieldsType, 'body' | 'deletedForEveryone'>
): boolean {
return !message.deletedForEveryone && Boolean(message.body);
}
export function canDeleteForEveryone(
message: Pick<
MessageWithUIFieldsType,

View file

@ -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}