mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-10-13 06:36:18 +08:00
* Some UX improvements (plus .gitignore tweak)
Made a few UX improvements:
- The send later delay can be skipped by clicking the new `Send now instead` button (localization required)
- If there is a range selection in the subject line then a context menu pops up on right click (for those that are allergic to keyboard shortcuts 😂)
- Message participant tweaks:
- you can now open the context menu on the name or email, not just the email
- you don't need to expand the participants to open the context menu
- name is shown over the email for `Email "Name else Email"` context menu item
- added `Copy "Email"` to context menu when nothing is selected/highlighted and just `Copy` when something is
- **for devs:** pretty console messages now consider your device theme and use an appropriate colour
* Prettier adjustments
* Revert `.gitignore` change
* Fix & actual prettier-ing
- Fix email context menu (now email and name can be alt-clicked)
- Sorted auto format (sorry didn't realise it was off)
* Use localised date/time format
Use a localised date format rather than forcing the Americanised format. This does loose the ordinal in `fullTimeString(Date)`
* Use weekday(Mon-Sun), for recent emails
- Changed recent emails to:
- display weekday and time (e.g Mon, 10:15)
- the recent emails to be 5 days, rather than 2
- Removed dead code
- Removed unnecessary import
* Remove unnecessary log
* Add key to prevent console error
Added a key to prevent a `Each child in an array or iterator should have a unique "key" prop` error in the console
* Add a key to "more" span
Co-authored-by: Ben Gotow <ben@foundry376.com>
215 lines
5.9 KiB
TypeScript
215 lines
5.9 KiB
TypeScript
import _ from 'underscore';
|
|
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;
|
|
|
|
interface MessageParticipantsProps {
|
|
to: Contact[];
|
|
cc: Contact[];
|
|
bcc: Contact[];
|
|
replyTo: Contact[];
|
|
from: Contact[];
|
|
onClick?: (e: React.MouseEvent<any>) => void;
|
|
isDetailed: boolean;
|
|
}
|
|
export default class MessageParticipants extends React.Component<MessageParticipantsProps> {
|
|
static displayName = 'MessageParticipants';
|
|
|
|
static defaultProps = {
|
|
to: [],
|
|
cc: [],
|
|
bcc: [],
|
|
from: [],
|
|
replyTo: [],
|
|
};
|
|
|
|
// Helpers
|
|
|
|
_allToParticipants() {
|
|
return _.union(this.props.to, this.props.cc, this.props.bcc);
|
|
}
|
|
|
|
_shortNames(contacts = [], max = MAX_COLLAPSED) {
|
|
let names = contacts.map((c, i) => (
|
|
<span key={`contact-${i}`}>
|
|
{i > 0 && ', '}
|
|
<span onContextMenu={() => this._onContactContextMenu(c)}>
|
|
{c.displayName({
|
|
includeAccountLabel: true,
|
|
compact: !AppEnv.config.get('core.reading.detailedNames'),
|
|
})}
|
|
</span>
|
|
</span>
|
|
));
|
|
|
|
if (names.length > max) {
|
|
const extra = names.length - max;
|
|
names = names.slice(0, max);
|
|
names.push(<span key="contact-more">and ${extra} more</span>);
|
|
}
|
|
|
|
return names;
|
|
}
|
|
|
|
_onSelectText = e => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
const textNode = e.currentTarget.childNodes[0];
|
|
const range = document.createRange();
|
|
range.setStart(textNode, 0);
|
|
range.setEnd(textNode, textNode.length);
|
|
const selection = document.getSelection();
|
|
selection.removeAllRanges();
|
|
selection.addRange(range);
|
|
};
|
|
|
|
_onContactContextMenu = contact => {
|
|
const menu = new Menu();
|
|
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.name ?? contact.email}`,
|
|
click: () => Actions.composeNewDraftToRecipient(contact),
|
|
})
|
|
);
|
|
menu.popup({});
|
|
};
|
|
|
|
_renderFullContacts(contacts = []) {
|
|
return contacts.map((c, i) => {
|
|
let comma = ',';
|
|
if (contacts.length === 1 || i === contacts.length - 1) {
|
|
comma = '';
|
|
}
|
|
|
|
if (c.name && c.name.length > 0 && c.name !== c.email) {
|
|
return (
|
|
<div key={`${c.email}-${i}`} className="participant selectable">
|
|
<div
|
|
className="participant-primary"
|
|
onClick={this._onSelectText}
|
|
onContextMenu={() => this._onContactContextMenu(c)}
|
|
>
|
|
{c.fullName()}
|
|
</div>
|
|
<div className="participant-secondary">
|
|
{' <'}
|
|
<span
|
|
onClick={this._onSelectText}
|
|
onContextMenu={() => this._onContactContextMenu(c)}
|
|
>
|
|
{c.email}
|
|
</span>
|
|
{`>${comma}`}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
return (
|
|
<div key={`${c.email}-${i}`} className="participant selectable">
|
|
<div className="participant-primary">
|
|
<span onClick={this._onSelectText} onContextMenu={() => this._onContactContextMenu(c)}>
|
|
{c.email}
|
|
</span>
|
|
{comma}
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|
|
}
|
|
|
|
_renderExpandedField(name, label, field, { includeLabel = true } = {}) {
|
|
return (
|
|
<div className="participant-type" key={`participant-type-${name}`}>
|
|
{includeLabel ? (
|
|
<div className={`participant-label ${name}-label`}>{label}: </div>
|
|
) : null}
|
|
<div className={`participant-name ${name}-contact`}>{this._renderFullContacts(field)}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
_renderExpanded() {
|
|
const { from, replyTo, to, cc, bcc } = this.props;
|
|
|
|
const expanded = [];
|
|
|
|
if (from.length > 0) {
|
|
expanded.push(
|
|
this._renderExpandedField('from', localized('From'), from, { includeLabel: false })
|
|
);
|
|
}
|
|
|
|
if (replyTo.length > 0) {
|
|
expanded.push(this._renderExpandedField('reply-to', localized('Reply to'), replyTo));
|
|
}
|
|
|
|
if (to.length > 0) {
|
|
expanded.push(this._renderExpandedField('to', localized('To'), to));
|
|
}
|
|
|
|
if (cc.length > 0) {
|
|
expanded.push(this._renderExpandedField('cc', localized('Cc'), cc));
|
|
}
|
|
|
|
if (bcc.length > 0) {
|
|
expanded.push(this._renderExpandedField('bcc', localized('Bcc'), bcc));
|
|
}
|
|
|
|
return <div className="expanded-participants">{expanded}</div>;
|
|
}
|
|
|
|
_renderCollapsed() {
|
|
const childSpans = [];
|
|
const toParticipants = this._allToParticipants();
|
|
|
|
if (this.props.from.length > 0) {
|
|
childSpans.push(
|
|
<span className="participant-name from-contact" key="from">
|
|
{this._shortNames(this.props.from)}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
if (toParticipants.length > 0) {
|
|
childSpans.push(
|
|
<span className="participant-label to-label" key="to-label">
|
|
{localized('To')}:
|
|
</span>,
|
|
<span className="participant-name to-contact" key="to-value">
|
|
{this._shortNames(toParticipants)}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
return <span className="collapsed-participants">{childSpans}</span>;
|
|
}
|
|
|
|
render() {
|
|
const { isDetailed, from, onClick } = this.props;
|
|
const classSet = classnames({
|
|
participants: true,
|
|
'message-participants': true,
|
|
collapsed: !isDetailed,
|
|
'from-participants': from.length > 0,
|
|
'to-participants': this._allToParticipants().length > 0,
|
|
});
|
|
|
|
return (
|
|
<div className={classSet} onClick={onClick}>
|
|
{isDetailed ? this._renderExpanded() : this._renderCollapsed()}
|
|
</div>
|
|
);
|
|
}
|
|
}
|