webmail: better save/close/cancel buttons in compose window

- keep them on the right side of the window (more important now that we can resize)
- merge the close & cancel buttons into a close button, with a popup asking what to do for changes not saved as draft.
This commit is contained in:
Mechiel Lukkien 2024-05-09 11:46:00 +02:00
parent 76aa96ab6f
commit 4d28a02621
No known key found for this signature in database
2 changed files with 23 additions and 36 deletions

View file

@ -2443,7 +2443,7 @@ const cmdHelp = async () => {
].map(t => dom.tr(dom.td(t[0]), dom.td(t[1]))))), dom.div(style({ width: '40em' }), dom.table(dom.tr(dom.td(attr.colspan('2'), dom.h2('Compose', style({ margin: '0' })))), [ ].map(t => dom.tr(dom.td(t[0]), dom.td(t[1]))))), dom.div(style({ width: '40em' }), dom.table(dom.tr(dom.td(attr.colspan('2'), dom.h2('Compose', style({ margin: '0' })))), [
['ctrl Enter', 'send message'], ['ctrl Enter', 'send message'],
['ctrl shift Enter', 'send message and archive thread'], ['ctrl shift Enter', 'send message and archive thread'],
['ctrl w', 'cancel message'], ['ctrl w', 'close message'],
['ctrl O', 'add To'], ['ctrl O', 'add To'],
['ctrl C', 'add Cc'], ['ctrl C', 'add Cc'],
['ctrl B', 'add Bcc'], ['ctrl B', 'add Bcc'],
@ -2624,17 +2624,20 @@ const compose = (opts, listMailboxes) => {
// when focus is on a textarea or not any specific UI element. So this isn't always // when focus is on a textarea or not any specific UI element. So this isn't always
// triggered. But we still have the beforeunload handler that checks for // triggered. But we still have the beforeunload handler that checks for
// unsavedChanges to protect the user in such cases. // unsavedChanges to protect the user in such cases.
const cmdCancel = async () => { const cmdClose = async () => {
draftCancelSave(); draftCancelSave();
await draftSavePromise; await draftSavePromise;
if (unsavedChanges()) { if (unsavedChanges()) {
const action = await new Promise((resolve) => { const action = await new Promise((resolve) => {
const remove = popup(dom.p('Message has unsaved changes.'), dom.br(), dom.div(dom.clickbutton('Save draft', function click() { const remove = popup(dom.p(dom.b('Message has unsaved changes')), dom.br(), dom.div(dom.clickbutton('Save draft', function click() {
resolve('save'); resolve('save');
remove(); remove();
}), ' ', dom.clickbutton('Remove draft', function click() { }), ' ', draftMessageID ? dom.clickbutton('Remove draft', function click() {
resolve('remove'); resolve('remove');
remove(); remove();
}) : [], ' ', dom.clickbutton('Discard changes', function click() {
resolve('discard');
remove();
}), ' ', dom.clickbutton('Cancel', function click() { }), ' ', dom.clickbutton('Cancel', function click() {
resolve('cancel'); resolve('cancel');
remove(); remove();
@ -2648,22 +2651,13 @@ const compose = (opts, listMailboxes) => {
await withStatus('Removing draft', client.MessageDelete([draftMessageID])); await withStatus('Removing draft', client.MessageDelete([draftMessageID]));
} }
} }
else { else if (action === 'cancel') {
return; return;
} }
} }
composeElem.remove(); composeElem.remove();
composeView = null; composeView = null;
}; };
const cmdClose = async () => {
draftCancelSave();
await draftSavePromise;
if (unsavedChanges()) {
await withStatus('Saving draft', draftSave());
}
composeElem.remove();
composeView = null;
};
const cmdSave = async () => { const cmdSave = async () => {
draftCancelSave(); draftCancelSave();
await draftSavePromise; await draftSavePromise;
@ -2740,7 +2734,7 @@ const compose = (opts, listMailboxes) => {
const shortcuts = { const shortcuts = {
'ctrl Enter': cmdSend, 'ctrl Enter': cmdSend,
'ctrl shift Enter': cmdSendArchive, 'ctrl shift Enter': cmdSendArchive,
'ctrl w': cmdCancel, 'ctrl w': cmdClose,
'ctrl O': cmdAddTo, 'ctrl O': cmdAddTo,
'ctrl C': cmdAddCc, 'ctrl C': cmdAddCc,
'ctrl B': cmdAddBcc, 'ctrl B': cmdAddBcc,
@ -3010,10 +3004,9 @@ const compose = (opts, listMailboxes) => {
flexGrow: '1', flexGrow: '1',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
}), dom.table(style({ width: '100%' }), dom.tr(dom.td(composeTextMildStyle, dom.span('From:')), dom.td(dom.div(style({ display: 'flex', gap: '1em' }), dom.div(from = dom.select(attr.required(''), style({ width: 'auto' }), fromOptions), ' ', toBtn = dom.clickbutton('To', clickCmd(cmdAddTo, shortcuts)), ' ', ccBtn = dom.clickbutton('Cc', clickCmd(cmdAddCc, shortcuts)), ' ', bccBtn = dom.clickbutton('Bcc', clickCmd(cmdAddBcc, shortcuts)), ' ', replyToBtn = dom.clickbutton('ReplyTo', clickCmd(cmdReplyTo, shortcuts)), ' ', customFromBtn = dom.clickbutton('From', attr.title('Set custom From address/name.'), clickCmd(cmdCustomFrom, shortcuts))), dom.div(listMailboxes().find(mb => mb.Draft) ? [ }), dom.table(style({ width: '100%' }), dom.tr(dom.td(composeTextMildStyle, dom.span('From:')), dom.td(dom.div(css('composeButtonsSpread', { display: 'flex', gap: '1em', justifyContent: 'space-between' }), dom.div(from = dom.select(attr.required(''), style({ width: 'auto' }), fromOptions), ' ', toBtn = dom.clickbutton('To', clickCmd(cmdAddTo, shortcuts)), ' ', ccBtn = dom.clickbutton('Cc', clickCmd(cmdAddCc, shortcuts)), ' ', bccBtn = dom.clickbutton('Bcc', clickCmd(cmdAddBcc, shortcuts)), ' ', replyToBtn = dom.clickbutton('ReplyTo', clickCmd(cmdReplyTo, shortcuts)), ' ', customFromBtn = dom.clickbutton('From', attr.title('Set custom From address/name.'), clickCmd(cmdCustomFrom, shortcuts))), dom.div(listMailboxes().find(mb => mb.Draft) ? [
dom.clickbutton('Save', attr.title('Save draft message.'), clickCmd(cmdSave, shortcuts)), ' ', dom.clickbutton('Save', attr.title('Save draft message.'), clickCmd(cmdSave, shortcuts)), ' ',
dom.clickbutton('Close', attr.title('Close window, saving draft message if body has changed or a draft was saved earlier.'), clickCmd(cmdClose, shortcuts)), ' ', ] : [], dom.clickbutton('Close', attr.title('Close window, saving draft message if body has changed or a draft was saved earlier.'), clickCmd(cmdClose, shortcuts)))))), toRow = dom.tr(dom.td('To:', composeTextMildStyle), toCell = dom.td(composeCellStyle)), replyToRow = dom.tr(dom.td('Reply-To:', composeTextMildStyle), replyToCell = dom.td(composeCellStyle)), ccRow = dom.tr(dom.td('Cc:', composeTextMildStyle), ccCell = dom.td(composeCellStyle)), bccRow = dom.tr(dom.td('Bcc:', composeTextMildStyle), bccCell = dom.td(composeCellStyle)), dom.tr(dom.td('Subject:', composeTextMildStyle), dom.td(subjectAutosize = dom.span(dom._class('autosize'), style({ width: '100%' }), // Without 100% width, the span takes minimal width for input, we want the full table cell.
] : [], dom.clickbutton('Cancel', attr.title('Close window, discarding (draft) message.'), clickCmd(cmdCancel, shortcuts)))))), toRow = dom.tr(dom.td('To:', composeTextMildStyle), toCell = dom.td(composeCellStyle)), replyToRow = dom.tr(dom.td('Reply-To:', composeTextMildStyle), replyToCell = dom.td(composeCellStyle)), ccRow = dom.tr(dom.td('Cc:', composeTextMildStyle), ccCell = dom.td(composeCellStyle)), bccRow = dom.tr(dom.td('Bcc:', composeTextMildStyle), bccCell = dom.td(composeCellStyle)), dom.tr(dom.td('Subject:', composeTextMildStyle), dom.td(subjectAutosize = dom.span(dom._class('autosize'), style({ width: '100%' }), // Without 100% width, the span takes minimal width for input, we want the full table cell.
subject = dom.input(style({ width: '100%' }), attr.value(opts.subject || ''), attr.required(''), focusPlaceholder('subject...'), function input() { subject = dom.input(style({ width: '100%' }), attr.value(opts.subject || ''), attr.required(''), focusPlaceholder('subject...'), function input() {
subjectAutosize.dataset.value = subject.value; subjectAutosize.dataset.value = subject.value;
}))))), body = dom.textarea(dom._class('mono'), style({ }))))), body = dom.textarea(dom._class('mono'), style({

View file

@ -1236,7 +1236,7 @@ const cmdHelp = async () => {
[ [
['ctrl Enter', 'send message'], ['ctrl Enter', 'send message'],
['ctrl shift Enter', 'send message and archive thread'], ['ctrl shift Enter', 'send message and archive thread'],
['ctrl w', 'cancel message'], ['ctrl w', 'close message'],
['ctrl O', 'add To'], ['ctrl O', 'add To'],
['ctrl C', 'add Cc'], ['ctrl C', 'add Cc'],
['ctrl B', 'add Bcc'], ['ctrl B', 'add Bcc'],
@ -1506,22 +1506,26 @@ const compose = (opts: ComposeOptions, listMailboxes: listMailboxes) => {
// when focus is on a textarea or not any specific UI element. So this isn't always // when focus is on a textarea or not any specific UI element. So this isn't always
// triggered. But we still have the beforeunload handler that checks for // triggered. But we still have the beforeunload handler that checks for
// unsavedChanges to protect the user in such cases. // unsavedChanges to protect the user in such cases.
const cmdCancel = async () => { const cmdClose = async () => {
draftCancelSave() draftCancelSave()
await draftSavePromise await draftSavePromise
if (unsavedChanges()) { if (unsavedChanges()) {
const action = await new Promise<string>((resolve) => { const action = await new Promise<string>((resolve) => {
const remove = popup( const remove = popup(
dom.p('Message has unsaved changes.'), dom.p(dom.b('Message has unsaved changes')),
dom.br(), dom.br(),
dom.div( dom.div(
dom.clickbutton('Save draft', function click() { dom.clickbutton('Save draft', function click() {
resolve('save') resolve('save')
remove() remove()
}), ' ', }), ' ',
dom.clickbutton('Remove draft', function click() { draftMessageID ? dom.clickbutton('Remove draft', function click() {
resolve('remove') resolve('remove')
remove() remove()
}) : [], ' ',
dom.clickbutton('Discard changes', function click() {
resolve('discard')
remove()
}), ' ', }), ' ',
dom.clickbutton('Cancel', function click() { dom.clickbutton('Cancel', function click() {
resolve('cancel') resolve('cancel')
@ -1536,7 +1540,7 @@ const compose = (opts: ComposeOptions, listMailboxes: listMailboxes) => {
if (draftMessageID) { if (draftMessageID) {
await withStatus('Removing draft', client.MessageDelete([draftMessageID])) await withStatus('Removing draft', client.MessageDelete([draftMessageID]))
} }
} else { } else if (action === 'cancel') {
return return
} }
} }
@ -1544,15 +1548,6 @@ const compose = (opts: ComposeOptions, listMailboxes: listMailboxes) => {
composeView = null composeView = null
} }
const cmdClose = async () => {
draftCancelSave()
await draftSavePromise
if (unsavedChanges()) {
await withStatus('Saving draft', draftSave())
}
composeElem.remove()
composeView = null
}
const cmdSave = async () => { const cmdSave = async () => {
draftCancelSave() draftCancelSave()
await draftSavePromise await draftSavePromise
@ -1637,7 +1632,7 @@ const compose = (opts: ComposeOptions, listMailboxes: listMailboxes) => {
const shortcuts: {[key: string]: command} = { const shortcuts: {[key: string]: command} = {
'ctrl Enter': cmdSend, 'ctrl Enter': cmdSend,
'ctrl shift Enter': cmdSendArchive, 'ctrl shift Enter': cmdSendArchive,
'ctrl w': cmdCancel, 'ctrl w': cmdClose,
'ctrl O': cmdAddTo, 'ctrl O': cmdAddTo,
'ctrl C': cmdAddCc, 'ctrl C': cmdAddCc,
'ctrl B': cmdAddBcc, 'ctrl B': cmdAddBcc,
@ -1966,7 +1961,7 @@ const compose = (opts: ComposeOptions, listMailboxes: listMailboxes) => {
), ),
dom.td( dom.td(
dom.div( dom.div(
style({display: 'flex', gap: '1em'}), css('composeButtonsSpread', {display: 'flex', gap: '1em', justifyContent: 'space-between'}),
dom.div( dom.div(
from=dom.select( from=dom.select(
attr.required(''), attr.required(''),
@ -1983,9 +1978,8 @@ const compose = (opts: ComposeOptions, listMailboxes: listMailboxes) => {
dom.div( dom.div(
listMailboxes().find(mb => mb.Draft) ? [ listMailboxes().find(mb => mb.Draft) ? [
dom.clickbutton('Save', attr.title('Save draft message.'), clickCmd(cmdSave, shortcuts)), ' ', dom.clickbutton('Save', attr.title('Save draft message.'), clickCmd(cmdSave, shortcuts)), ' ',
dom.clickbutton('Close', attr.title('Close window, saving draft message if body has changed or a draft was saved earlier.'), clickCmd(cmdClose, shortcuts)), ' ',
] : [], ] : [],
dom.clickbutton('Cancel', attr.title('Close window, discarding (draft) message.'), clickCmd(cmdCancel, shortcuts)), dom.clickbutton('Close', attr.title('Close window, saving draft message if body has changed or a draft was saved earlier.'), clickCmd(cmdClose, shortcuts)),
), ),
), ),
), ),