Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Some UX improvements #2425

Merged
merged 9 commits into from
Sep 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion app/internal_packages/message-list/lib/message-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -351,7 +352,9 @@ class MessageList extends React.Component<Record<string, unknown>, MessageListSt
<div className="message-subject-wrap">
<MailImportantIcon thread={this.state.currentThread} />
<div style={{ flex: 1 }}>
<span className="message-subject">{subject}</span>
<span className="message-subject" onContextMenu={() => _onSubjectContextMenu()}>
{subject}
</span>
<MailLabelSet
removable
includeCurrentCategories
Expand All @@ -369,6 +372,14 @@ class MessageList extends React.Component<Record<string, unknown>, MessageListSt
/>
</div>
);

function _onSubjectContextMenu() {
if (window.getSelection()?.type == 'Range') {
const menu = new Menu();
menu.append(new MenuItem({ role: 'copy' }));
menu.popup({});
}
}
}

_renderMinifiedBundle(bundle) {
Expand Down
41 changes: 29 additions & 12 deletions app/internal_packages/message-list/lib/message-participants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -34,18 +33,25 @@ export default class MessageParticipants extends React.Component<MessageParticip
}

_shortNames(contacts = [], max = MAX_COLLAPSED) {
let names = contacts.map(c =>
c.displayName({
includeAccountLabel: true,
compact: !AppEnv.config.get('core.reading.detailedNames'),
})
);
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(`and ${extra} more`);
names.push(<span key="contact-more">and ${extra} more</span>);
}
return names.join(', ');

return names;
}

_onSelectText = e => {
Expand All @@ -63,10 +69,17 @@ export default class MessageParticipants extends React.Component<MessageParticip

_onContactContextMenu = contact => {
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),
})
);
Expand All @@ -83,7 +96,11 @@ export default class MessageParticipants extends React.Component<MessageParticip
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}>
<div
className="participant-primary"
onClick={this._onSelectText}
onContextMenu={() => this._onContactContextMenu(c)}
>
{c.fullName()}
</div>
<div className="participant-secondary">
Expand Down
30 changes: 29 additions & 1 deletion app/internal_packages/undo-redo/lib/undo-redo-toast.tsx
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -11,6 +12,29 @@ function isUndoSend(block) {
);
}

async function sendMessageNow(block) {
if (isUndoSend(block)) {
const message = await DatabaseStore.find<Message>(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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good, cool that updating the metadata actually causes it to send immediately. I'll try this a couple times once we merge just to stress test it a bit, but it seems safe to me.


return true;
}

return false;
}

function getUndoSendExpiration(block) {
return block.tasks[0].value.expiration * 1000;
}
Expand Down Expand Up @@ -75,6 +99,10 @@ const UndoSendContent = ({ block, onMouseEnter, onMouseLeave }) => {
<div className="content" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<Countdown expiration={getUndoSendExpiration(block)} />
<div className="message">{localized('Sending soon...')}</div>
<div className="action" onClick={async () => { await sendMessageNow(block) && onMouseLeave() }}>
<RetinaImg name="icon-composer-send.png" mode={RetinaImg.Mode.ContentIsMask} />
<span className="send-action-text">{localized('Send now instead')}</span>
</div>
<div className="action" onClick={() => AppEnv.commands.dispatch('core:undo')}>
<RetinaImg name="undo-icon@2x.png" mode={RetinaImg.Mode.ContentIsMask} />
<span className="undo-action-text">{localized('Undo')}</span>
Expand Down
3 changes: 3 additions & 0 deletions app/internal_packages/undo-redo/styles/index.less
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions app/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
71 changes: 47 additions & 24 deletions app/src/date-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,27 +389,40 @@ const DateUtils = {
*
* The returned date/time format depends on how long ago the timestamp is.
*/
shortTimeString(datetime) {
shortTimeString(datetime: Date) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This all looks great, thank you for updating it to use the new browser standard APIs!

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);
},

/**
Expand All @@ -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,
});
},

/**
Expand All @@ -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 });

return moment(datetime)
.tz(tz)
.format(format);
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
// `(?<!\d)${dateTime.getDay()}(?!\d)` replace `$1${localise(ordinal)}`

return datetime.toLocaleTimeString(navigator.language, {
hour12: !AppEnv.config.get('core.workspace.use24HourClock'),
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long',
second: undefined,
});
},
};

Expand Down
11 changes: 8 additions & 3 deletions app/src/flux/stores/database-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,14 @@ class DatabaseStore extends MailspringStore {
}

_prettyConsoleLog(qa) {
const darkTheme =
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches,
primaryColor = darkTheme ? 'white' : 'black',
purpleColor = darkTheme ? 'pink' : 'purple';

let q = qa.replace(/%/g, '%%');
q = `color:black |||%c ${q}`;
q = q.replace(/`(\w+)`/g, '||| color:purple |||%c$&||| color:black |||%c');
q = `color:${primaryColor} |||%c ${q}`;
q = q.replace(/`(\w+)`/g, `||| color:${purpleColor} |||%c$&||| color:${primaryColor} |||%c`);

const colorRules = {
'color:green': [
Expand All @@ -184,7 +189,7 @@ class DatabaseStore extends MailspringStore {
for (const keyword of colorRules[style]) {
q = q.replace(
new RegExp(`\\b${keyword}\\b`, 'g'),
`||| ${style} |||%c${keyword}||| color:black |||%c`
`||| ${style} |||%c${keyword}||| color:${primaryColor} |||%c`
);
}
}
Expand Down