diff --git a/app/internal_packages/message-list/lib/message-list.tsx b/app/internal_packages/message-list/lib/message-list.tsx index 448f4813b..78da037f1 100644 --- a/app/internal_packages/message-list/lib/message-list.tsx +++ b/app/internal_packages/message-list/lib/message-list.tsx @@ -40,6 +40,7 @@ interface MessageListState { minified: boolean; } +const { Menu, MenuItem } = require('@electron/remote'); const PREF_REPLY_TYPE = 'core.sending.defaultReplyType'; const PREF_RESTRICT_WIDTH = 'core.reading.restrictMaxWidth'; const PREF_DESCENDING_ORDER = 'core.reading.descendingOrderMessageList'; @@ -351,7 +352,9 @@ class MessageList extends React.Component, MessageListSt
- {subject} + _onSubjectContextMenu()}> + {subject} + , MessageListSt />
); + + function _onSubjectContextMenu() { + if (window.getSelection()?.type == 'Range') { + const menu = new Menu(); + menu.append(new MenuItem({ role: 'copy' })); + menu.popup({}); + } + } } _renderMinifiedBundle(bundle) { diff --git a/app/internal_packages/message-list/lib/message-participants.tsx b/app/internal_packages/message-list/lib/message-participants.tsx index ff0ace61b..aec72cdbb 100644 --- a/app/internal_packages/message-list/lib/message-participants.tsx +++ b/app/internal_packages/message-list/lib/message-participants.tsx @@ -3,7 +3,6 @@ import classnames from 'classnames'; import React from 'react'; import { localized, Actions, Contact } from 'mailspring-exports'; - const { Menu, MenuItem } = require('@electron/remote'); const MAX_COLLAPSED = 5; @@ -34,18 +33,25 @@ export default class MessageParticipants extends React.Component - c.displayName({ - includeAccountLabel: true, - compact: !AppEnv.config.get('core.reading.detailedNames'), - }) - ); + let names = contacts.map((c, i) => ( + + {i > 0 && ', '} + this._onContactContextMenu(c)}> + {c.displayName({ + includeAccountLabel: true, + compact: !AppEnv.config.get('core.reading.detailedNames'), + })} + + + )); + if (names.length > max) { const extra = names.length - max; names = names.slice(0, max); - names.push(`and ${extra} more`); + names.push(and ${extra} more); } - return names.join(', '); + + return names; } _onSelectText = e => { @@ -63,10 +69,17 @@ export default class MessageParticipants extends React.Component { const menu = new Menu(); - menu.append(new MenuItem({ role: 'copy' })); + menu.append( + window.getSelection()?.type == 'Range' + ? new MenuItem({ role: 'copy' }) + : new MenuItem({ + label: `${localized(`Copy`)} "${contact.email}"`, + click: () => navigator.clipboard.writeText(contact.email), + }) + ); menu.append( new MenuItem({ - label: `${localized(`Email`)} ${contact.email}`, + label: `${localized(`Email`)} ${contact.name ?? contact.email}`, click: () => Actions.composeNewDraftToRecipient(contact), }) ); @@ -83,7 +96,11 @@ export default class MessageParticipants extends React.Component 0 && c.name !== c.email) { return (
-
+
this._onContactContextMenu(c)} + > {c.fullName()}
diff --git a/app/internal_packages/undo-redo/lib/undo-redo-toast.tsx b/app/internal_packages/undo-redo/lib/undo-redo-toast.tsx index d0dbbe14b..340e39ec2 100644 --- a/app/internal_packages/undo-redo/lib/undo-redo-toast.tsx +++ b/app/internal_packages/undo-redo/lib/undo-redo-toast.tsx @@ -1,7 +1,8 @@ import React from 'react'; -import { localized, UndoRedoStore, SyncbackMetadataTask } from 'mailspring-exports'; +import { localized, UndoRedoStore, SyncbackMetadataTask, DatabaseStore, Message, Actions } from 'mailspring-exports'; import { RetinaImg } from 'mailspring-component-kit'; import { CSSTransitionGroup } from 'react-transition-group'; +import { PLUGIN_ID } from '../../../internal_packages/send-later/lib/send-later-constants'; function isUndoSend(block) { return ( @@ -11,6 +12,29 @@ function isUndoSend(block) { ); } +async function sendMessageNow(block) { + if (isUndoSend(block)) { + const message = await DatabaseStore.find(Message, (block.tasks[0] as SyncbackMetadataTask).modelId), + newExpiry = Math.floor(Date.now() / 1000); + + Actions.queueTask( + SyncbackMetadataTask.forSaving({ + model: message, + pluginId: PLUGIN_ID, + value: { + expiration: newExpiry, + }, + }) + ); + + block.tasks[0].value.expiration = newExpiry; + + return true; + } + + return false; +} + function getUndoSendExpiration(block) { return block.tasks[0].value.expiration * 1000; } @@ -75,6 +99,10 @@ const UndoSendContent = ({ block, onMouseEnter, onMouseLeave }) => {
{localized('Sending soon...')}
+
{ await sendMessageNow(block) && onMouseLeave() }}> + + {localized('Send now instead')} +
AppEnv.commands.dispatch('core:undo')}> {localized('Undo')} diff --git a/app/internal_packages/undo-redo/styles/index.less b/app/internal_packages/undo-redo/styles/index.less index 7448ff93f..bef6735f9 100644 --- a/app/internal_packages/undo-redo/styles/index.less +++ b/app/internal_packages/undo-redo/styles/index.less @@ -46,10 +46,13 @@ img { background-color: @background-primary; } + &:hover { background: fade(@black, 30%); border: 1px solid fade(@background-primary, 30%); } + + .send-action-text, .undo-action-text { margin-left: 5px; color: @background-primary; diff --git a/app/lang/en.json b/app/lang/en.json index 1569614c8..6caf3cee3 100644 --- a/app/lang/en.json +++ b/app/lang/en.json @@ -617,6 +617,7 @@ "Send message": "Send message", "Send more than one message using the same %@ or subject line to compare open rates and reply rates.": "Send more than one message using the same %@ or subject line to compare open rates and reply rates.", "Send new messages from:": "Send new messages from:", + "Send now instead": "Send now instead", "Send on your own schedule": "Send on your own schedule", "Sender Name": "Sender Name", "Sending": "Sending", diff --git a/app/src/date-utils.ts b/app/src/date-utils.ts index 04ddb03af..9fff6ed6a 100644 --- a/app/src/date-utils.ts +++ b/app/src/date-utils.ts @@ -389,27 +389,40 @@ const DateUtils = { * * The returned date/time format depends on how long ago the timestamp is. */ - shortTimeString(datetime) { + shortTimeString(datetime: Date) { const now = moment(); const diff = now.diff(datetime, 'days', true); const isSameDay = now.isSame(datetime, 'days'); - let format = null; + const opts: Intl.DateTimeFormatOptions = { + hour12: !AppEnv.config.get('core.workspace.use24HourClock'), + }; if (diff <= 1 && isSameDay) { // Time if less than 1 day old - format = DateUtils.getTimeFormat(null); - } else if (diff < 2 && !isSameDay) { - // Month and day with time if up to 2 days ago - format = `MMM D, ${DateUtils.getTimeFormat(null)}`; - } else if (diff >= 2 && diff < 365) { - // Month and day up to 1 year old - format = 'MMM D'; + opts.hour = 'numeric'; + opts.minute = '2-digit'; + } else if (diff < 5 && !isSameDay) { + // Weekday with time if up to 2 days ago + //opts.month = 'short'; + //opts.day = 'numeric'; + opts.weekday = 'short'; + opts.hour = 'numeric'; + opts.minute = '2-digit'; } else { - // Month, day and year if over a year old - format = 'MMM D YYYY'; + if (diff < 365) { + // Month and day up to 1 year old + opts.month = 'short'; + opts.day = 'numeric'; + } else { + // Month, day and year if over a year old + opts.year = 'numeric'; + opts.month = 'short'; + opts.day = 'numeric'; + } + return datetime.toLocaleDateString(navigator.language, opts); } - return moment(datetime).format(format); + return datetime.toLocaleTimeString(navigator.language, opts); }, /** @@ -418,11 +431,14 @@ const DateUtils = { * @param {Date} datetime - Timestamp * @return {String} Formated date/time */ - mediumTimeString(datetime) { - let format = 'MMMM D, YYYY, '; - format += DateUtils.getTimeFormat({ seconds: false, upperCase: true, timeZone: false }); - - return moment(datetime).format(format); + mediumTimeString(datetime: Date) { + return datetime.toLocaleTimeString(navigator.language, { + hour12: !AppEnv.config.get('core.workspace.use24HourClock'), + year: 'numeric', + month: 'long', + day: 'numeric', + second: undefined, + }); }, /** @@ -431,13 +447,20 @@ const DateUtils = { * @param {Date} datetime - Timestamp * @return {String} Formated date/time */ - fullTimeString(datetime) { - let format = 'dddd, MMMM Do YYYY, '; - format += DateUtils.getTimeFormat({ seconds: true, upperCase: true, timeZone: true }); + fullTimeString(datetime: Date) { + // ISSUE: this does drop ordinal. There is this: + // -> new Intl.PluralRules(LOCALE, { type: "ordinal" }).select(dateTime.getDay()) + // which may work with the below regex, though localisation is required + // `(?