Add copy option to triple-dot menu of messages
This commit is contained in:
parent
f1624705a7
commit
f004e714f0
9 changed files with 61 additions and 0 deletions
|
@ -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}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue