mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-05-08 08:58:38 +01:00
Add copy option to triple-dot menu of messages
This commit is contained in:
committed by
Josh Perez
parent
f1624705a7
commit
f004e714f0
@@ -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"
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user