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", "messageformat": "More Info",
"description": "Shown on the drop-down menu for an individual message, takes you to message detail screen" "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": { "icu:MessageContextMenu__select": {
"messageformat": "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" "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', id: 'some-id',
title: 'Person X', title: 'Person X',
}), }),
canCopy: true,
canEditMessage: true, canEditMessage: true,
canReact: true, canReact: true,
canReply: true, canReply: true,
@ -129,6 +130,7 @@ const defaultMessageProps: TimelineMessagesProps = {
setMessageToEdit: action('setMessageToEdit'), setMessageToEdit: action('setMessageToEdit'),
setQuoteByMessageId: action('default--setQuoteByMessageId'), setQuoteByMessageId: action('default--setQuoteByMessageId'),
retryMessageSend: action('default--retryMessageSend'), retryMessageSend: action('default--retryMessageSend'),
copyMessageText: action('copyMessageText'),
retryDeleteForEveryone: action('default--retryDeleteForEveryone'), retryDeleteForEveryone: action('default--retryDeleteForEveryone'),
saveAttachment: action('saveAttachment'), saveAttachment: action('saveAttachment'),
scrollToQuotedMessage: action('default--scrollToQuotedMessage'), scrollToQuotedMessage: action('default--scrollToQuotedMessage'),

View file

@ -47,6 +47,7 @@ function mockMessageTimelineItem(
data: { data: {
id, id,
author: getDefaultConversation({}), author: getDefaultConversation({}),
canCopy: true,
canDeleteForEveryone: false, canDeleteForEveryone: false,
canDownload: true, canDownload: true,
canEditMessage: true, canEditMessage: true,
@ -282,6 +283,7 @@ const actions = () => ({
reactToMessage: action('reactToMessage'), reactToMessage: action('reactToMessage'),
setMessageToEdit: action('setMessageToEdit'), setMessageToEdit: action('setMessageToEdit'),
setQuoteByMessageId: action('setQuoteByMessageId'), setQuoteByMessageId: action('setQuoteByMessageId'),
copyMessageText: action('copyMessageText'),
retryDeleteForEveryone: action('retryDeleteForEveryone'), retryDeleteForEveryone: action('retryDeleteForEveryone'),
retryMessageSend: action('retryMessageSend'), retryMessageSend: action('retryMessageSend'),
saveAttachment: action('saveAttachment'), saveAttachment: action('saveAttachment'),

View file

@ -69,6 +69,7 @@ const getDefaultProps = () => ({
clearTargetedMessage: action('clearTargetedMessage'), clearTargetedMessage: action('clearTargetedMessage'),
setMessageToEdit: action('setMessageToEdit'), setMessageToEdit: action('setMessageToEdit'),
setQuoteByMessageId: action('setQuoteByMessageId'), setQuoteByMessageId: action('setQuoteByMessageId'),
copyMessageText: action('copyMessageText'),
retryDeleteForEveryone: action('retryDeleteForEveryone'), retryDeleteForEveryone: action('retryDeleteForEveryone'),
retryMessageSend: action('retryMessageSend'), retryMessageSend: action('retryMessageSend'),
blockGroupLinkRequests: action('blockGroupLinkRequests'), blockGroupLinkRequests: action('blockGroupLinkRequests'),

View file

@ -245,6 +245,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
attachments: overrideProps.attachments, attachments: overrideProps.attachments,
author: overrideProps.author || getDefaultConversation(), author: overrideProps.author || getDefaultConversation(),
bodyRanges: overrideProps.bodyRanges, bodyRanges: overrideProps.bodyRanges,
canCopy: true,
canEditMessage: true, canEditMessage: true,
canReact: true, canReact: true,
canReply: true, canReply: true,
@ -324,6 +325,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
saveAttachment: action('saveAttachment'), saveAttachment: action('saveAttachment'),
setQuoteByMessageId: action('setQuoteByMessageId'), setQuoteByMessageId: action('setQuoteByMessageId'),
retryMessageSend: action('retryMessageSend'), retryMessageSend: action('retryMessageSend'),
copyMessageText: action('copyMessageText'),
retryDeleteForEveryone: action('retryDeleteForEveryone'), retryDeleteForEveryone: action('retryDeleteForEveryone'),
scrollToQuotedMessage: action('scrollToQuotedMessage'), scrollToQuotedMessage: action('scrollToQuotedMessage'),
targetMessage: action('targetMessage'), targetMessage: action('targetMessage'),

View file

@ -32,6 +32,7 @@ import type { DeleteMessagesPropsType } from '../../state/ducks/globalModals';
export type PropsData = { export type PropsData = {
canDownload: boolean; canDownload: boolean;
canCopy: boolean;
canEditMessage: boolean; canEditMessage: boolean;
canRetry: boolean; canRetry: boolean;
canRetryDeleteForEveryone: boolean; canRetryDeleteForEveryone: boolean;
@ -50,6 +51,7 @@ export type PropsActions = {
{ emoji, remove }: { emoji: string; remove: boolean } { emoji, remove }: { emoji: string; remove: boolean }
) => void; ) => void;
retryMessageSend: (id: string) => void; retryMessageSend: (id: string) => void;
copyMessageText: (id: string) => void;
retryDeleteForEveryone: (id: string) => void; retryDeleteForEveryone: (id: string) => void;
setMessageToEdit: (conversationId: string, messageId: string) => unknown; setMessageToEdit: (conversationId: string, messageId: string) => unknown;
setQuoteByMessageId: (conversationId: string, messageId: string) => void; setQuoteByMessageId: (conversationId: string, messageId: string) => void;
@ -82,6 +84,7 @@ export function TimelineMessage(props: Props): JSX.Element {
attachments, attachments,
author, author,
canDownload, canDownload,
canCopy,
canEditMessage, canEditMessage,
canReact, canReact,
canReply, canReply,
@ -101,6 +104,7 @@ export function TimelineMessage(props: Props): JSX.Element {
isTapToView, isTapToView,
kickOffAttachmentDownload, kickOffAttachmentDownload,
payment, payment,
copyMessageText,
pushPanelForConversation, pushPanelForConversation,
reactToMessage, reactToMessage,
renderEmojiPicker, renderEmojiPicker,
@ -367,6 +371,7 @@ export function TimelineMessage(props: Props): JSX.Element {
? () => retryDeleteForEveryone(id) ? () => retryDeleteForEveryone(id)
: undefined : undefined
} }
onCopy={canCopy ? () => copyMessageText(id) : undefined}
onSelect={() => toggleSelectMessage(conversationId, id, false, true)} onSelect={() => toggleSelectMessage(conversationId, id, false, true)}
onForward={ onForward={
canForward ? () => toggleForwardMessagesModal([id]) : undefined canForward ? () => toggleForwardMessagesModal([id]) : undefined
@ -554,6 +559,7 @@ type MessageContextProps = {
onReact: (() => void) | undefined; onReact: (() => void) | undefined;
onRetryMessageSend: (() => void) | undefined; onRetryMessageSend: (() => void) | undefined;
onRetryDeleteForEveryone: (() => void) | undefined; onRetryDeleteForEveryone: (() => void) | undefined;
onCopy: (() => void) | undefined;
onForward: (() => void) | undefined; onForward: (() => void) | undefined;
onDeleteMessage: () => void; onDeleteMessage: () => void;
onMoreInfo: () => void; onMoreInfo: () => void;
@ -569,6 +575,7 @@ const MessageContextMenu = ({
onReplyToMessage, onReplyToMessage,
onReact, onReact,
onMoreInfo, onMoreInfo,
onCopy,
onSelect, onSelect,
onRetryMessageSend, onRetryMessageSend,
onRetryDeleteForEveryone, onRetryDeleteForEveryone,
@ -667,6 +674,19 @@ const MessageContextMenu = ({
> >
{i18n('icu:MessageContextMenu__select')} {i18n('icu:MessageContextMenu__select')}
</MenuItem> </MenuItem>
{onCopy && (
<MenuItem
attributes={{
className:
'module-message__context--icon module-message__context__copy-timestamp',
}}
onClick={() => {
onCopy();
}}
>
{i18n('icu:copy')}
</MenuItem>
)}
<MenuItem <MenuItem
attributes={{ attributes={{
className: className:

View file

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

View file

@ -720,6 +720,7 @@ export const getPropsForMessage = (
storyReplyContext, storyReplyContext,
textAttachment, textAttachment,
payment, payment,
canCopy: canCopy(message),
canEditMessage: canEditMessage(message), canEditMessage: canEditMessage(message),
canDeleteForEveryone: canDeleteForEveryone(message), canDeleteForEveryone: canDeleteForEveryone(message),
canDownload: canDownload(message, conversationSelector), canDownload: canDownload(message, conversationSelector),
@ -1773,6 +1774,12 @@ export function canReact(
return canReplyOrReact(message, ourConversationId, conversation); return canReplyOrReact(message, ourConversationId, conversation);
} }
export function canCopy(
message: Pick<MessageWithUIFieldsType, 'body' | 'deletedForEveryone'>
): boolean {
return !message.deletedForEveryone && Boolean(message.body);
}
export function canDeleteForEveryone( export function canDeleteForEveryone(
message: Pick< message: Pick<
MessageWithUIFieldsType, MessageWithUIFieldsType,

View file

@ -118,6 +118,7 @@ export function SmartTimelineItem(props: ExternalProps): JSX.Element {
messageExpanded, messageExpanded,
openGiftBadge, openGiftBadge,
pushPanelForConversation, pushPanelForConversation,
copyMessageText,
retryDeleteForEveryone, retryDeleteForEveryone,
retryMessageSend, retryMessageSend,
saveAttachment, saveAttachment,
@ -184,6 +185,7 @@ export function SmartTimelineItem(props: ExternalProps): JSX.Element {
openGiftBadge={openGiftBadge} openGiftBadge={openGiftBadge}
pushPanelForConversation={pushPanelForConversation} pushPanelForConversation={pushPanelForConversation}
reactToMessage={reactToMessage} reactToMessage={reactToMessage}
copyMessageText={copyMessageText}
retryDeleteForEveryone={retryDeleteForEveryone} retryDeleteForEveryone={retryDeleteForEveryone}
retryMessageSend={retryMessageSend} retryMessageSend={retryMessageSend}
returnToActiveCall={returnToActiveCall} returnToActiveCall={returnToActiveCall}