Add setting to disable email content max width (#251)

commit 84d0997cdca895d321ed6e70f1ab40cf03b14aa0
Author: Ben Gotow <ben@foundry376.com>
Date:   Sun Jun 9 17:36:38 2019 -0500

    A bit of polish

commit 066963a8111d510cab2d87caaa547bdabe581461
Merge: 06d2e4546 8ed229a7c
Author: Ben Gotow <ben@foundry376.com>
Date:   Sun Jun 9 17:07:20 2019 -0500

    Merge branch 'emailWidthFix' of https://github.com/mattlyons0/Mailspring into mattlyons0-emailWidthFix

    # Conflicts:
    #	app/internal_packages/message-list/lib/message-list.tsx
    #	app/internal_packages/message-list/styles/message-list.less
    #	app/src/config-schema.es6

commit 8ed229a7c7
Author: Matt Lyons <matt@mattlyons.net>
Date:   Wed Oct 25 03:02:03 2017 -0700

    Add setting to disable email content max width

    Closes #228
This commit is contained in:
Matt Lyons 2019-06-09 17:38:57 -05:00
parent 06d2e4546c
commit f20e9c3f39
10 changed files with 189 additions and 134 deletions

View file

@ -165,8 +165,10 @@ export default class ComposerView extends React.Component<ComposerViewProps, Com
}
_renderContent() {
const restrictWidth = AppEnv.config.get('core.reading.restrictMaxWidth');
return (
<div className="composer-centered">
<div className={`composer-centered ${restrictWidth && 'restrict-width'}`}>
<ComposerHeader
ref={el => {
if (el) {
@ -333,8 +335,9 @@ export default class ComposerView extends React.Component<ComposerViewProps, Com
}
_renderActionsRegion() {
const restrictWidth = AppEnv.config.get('core.reading.restrictMaxWidth');
return (
<div className="composer-action-bar-content">
<div className={`composer-action-bar-content ${restrictWidth && 'restrict-width'}`}>
<ActionBarPlugins
draft={this.props.draft}
session={this.props.session}

View file

@ -286,9 +286,12 @@ body.platform-win32 {
display: flex;
margin: 0 auto;
flex-direction: row;
max-width: @compose-width;
padding: 9px 22.5px;
&.restrict-width {
max-width: @compose-width;
}
.composer-action-bar-plugins {
flex-wrap: wrap;
}
@ -338,9 +341,12 @@ body.platform-win32 {
flex-direction: column;
flex: 1;
width: 100%;
max-width: @compose-width;
margin: 0 auto;
padding-top: @spacing-standard * 0.7;
&.restrict-width {
max-width: @compose-width;
}
}
.composer-body-wrap {

View file

@ -84,6 +84,8 @@ export default class EmailFrame extends React.Component<EmailFrameProps> {
// the `border-collapse: collapse` css property while setting a
// `padding`.
const styles = EmailFrameStylesStore.styles();
const restrictWidth = AppEnv.config.get('core.reading.restrictMaxWidth');
doc.open();
doc.write(
`<!DOCTYPE html>` +
@ -92,6 +94,10 @@ export default class EmailFrame extends React.Component<EmailFrameProps> {
);
doc.close();
if (doc.body && restrictWidth) {
doc.body.classList.add('restrict-width');
}
// Notify the EventedIFrame that we've replaced it's document (with `open`)
// so it can attach event listeners again.
this._iframeComponent.didReplaceDocument();

View file

@ -1,13 +1,6 @@
import classNames from 'classnames';
import React from 'react';
import {
PropTypes,
Utils,
DraftStore,
ComponentRegistry,
Thread,
Message,
} from 'mailspring-exports';
import { Utils, DraftStore, ComponentRegistry, Thread, Message } from 'mailspring-exports';
import MessageItem from './message-item';
@ -21,9 +14,13 @@ interface MessageItemContainerProps {
scrollTo: () => void;
}
interface MessageItemContainerState {
isSending: boolean;
}
export default class MessageItemContainer extends React.Component<
MessageItemContainerProps,
{ isSending: boolean }
MessageItemContainerState
> {
static displayName = 'MessageItemContainer';

View file

@ -0,0 +1,49 @@
import React from 'react';
import { localized, PropTypes, Utils } from 'mailspring-exports';
export class MessageListScrollTooltip extends React.Component<
{ viewportCenter: number; totalHeight: number },
{ idx: number; count: number }
> {
static displayName = 'MessageListScrollTooltip';
static propTypes = {
viewportCenter: PropTypes.number.isRequired,
totalHeight: PropTypes.number.isRequired,
};
componentWillMount() {
this.setupForProps(this.props);
}
componentWillReceiveProps(newProps) {
this.setupForProps(newProps);
}
shouldComponentUpdate(newProps, newState) {
return !Utils.isEqualReact(this.state, newState);
}
setupForProps(props) {
// Technically, we could have MessageList provide the currently visible
// item index, but the DOM approach is simple and self-contained.
//
const els = document.querySelectorAll('.message-item-wrap');
let idx = Array.from(els).findIndex(el => (el as HTMLElement).offsetTop > props.viewportCenter);
if (idx === -1) {
idx = els.length;
}
this.setState({
idx: idx,
count: els.length,
});
}
render() {
return (
<div className="scroll-tooltip">
{localized('%1$@ of %2$@', this.state.idx, this.state.count)}
</div>
);
}
}

View file

@ -1,10 +1,8 @@
import classNames from 'classnames';
import React from 'react';
import ReactDOM from 'react-dom';
import {
localized,
PropTypes,
Utils,
Actions,
MessageStore,
@ -27,53 +25,8 @@ import {
import FindInThread from './find-in-thread';
import MessageItemContainer from './message-item-container';
class MessageListScrollTooltip extends React.Component<
{ viewportCenter: number; totalHeight: number },
{ idx: number; count: number }
> {
static displayName = 'MessageListScrollTooltip';
static propTypes = {
viewportCenter: PropTypes.number.isRequired,
totalHeight: PropTypes.number.isRequired,
};
componentWillMount() {
this.setupForProps(this.props);
}
componentWillReceiveProps(newProps) {
this.setupForProps(newProps);
}
shouldComponentUpdate(newProps, newState) {
return !Utils.isEqualReact(this.state, newState);
}
setupForProps(props) {
// Technically, we could have MessageList provide the currently visible
// item index, but the DOM approach is simple and self-contained.
//
const els = document.querySelectorAll('.message-item-wrap');
let idx = Array.from(els).findIndex(el => (el as HTMLElement).offsetTop > props.viewportCenter);
if (idx === -1) {
idx = els.length;
}
this.setState({
idx: idx,
count: els.length,
});
}
render() {
return (
<div className="scroll-tooltip">
{localized('%1$@ of %2$@', this.state.idx, this.state.count)}
</div>
);
}
}
import { MessageListScrollTooltip } from './message-list-scroll-tooltip';
import { SubjectLineIcons } from './subject-line-icons';
interface MessageListState {
messages: Message[];
@ -87,6 +40,10 @@ interface MessageListState {
minified: boolean;
}
const PREF_REPLY_TYPE = 'core.sending.defaultReplyType';
const PREF_RESTRICT_WIDTH = 'core.reading.restrictMaxWidth';
const PREF_DESCENDING_ORDER = 'core.reading.descendingOrderMessageList';
class MessageList extends React.Component<{}, MessageListState> {
static displayName = 'MessageList';
static containerStyles = {
@ -172,7 +129,7 @@ class MessageList extends React.Component<{}, MessageListState> {
// Returns either "reply" or "reply-all"
_replyType() {
const defaultReplyType = AppEnv.config.get('core.sending.defaultReplyType');
const defaultReplyType = AppEnv.config.get(PREF_REPLY_TYPE);
const lastMessage = this._lastMessage();
if (!lastMessage) {
return 'reply';
@ -232,7 +189,7 @@ class MessageList extends React.Component<{}, MessageListState> {
const hasReplyArea = mostRecentMessage && !mostRecentMessage.draft;
// Invert the message list if the descending option is set
if (AppEnv.config.get('core.reading.descendingOrderMessageList')) {
if (AppEnv.config.get(PREF_DESCENDING_ORDER)) {
messages = messages.reverse();
}
@ -396,61 +353,13 @@ class MessageList extends React.Component<{}, MessageListState> {
thread={this.state.currentThread}
/>
</div>
{this._renderIcons()}
</div>
);
}
_renderIcons() {
return (
<div className="message-icons-wrap">
{this._renderExpandToggle()}
<div onClick={this._onPrintThread}>
<RetinaImg
name="print.png"
title={localized('Print Thread')}
mode={RetinaImg.Mode.ContentIsMask}
/>
</div>
{this._renderPopoutToggle()}
</div>
);
}
_renderExpandToggle() {
if (!this.state.canCollapse) {
return <span />;
}
return (
<div onClick={this._onToggleAllMessagesExpanded}>
<RetinaImg
name={this.state.hasCollapsedItems ? 'expand.png' : 'collapse.png'}
title={this.state.hasCollapsedItems ? localized('Expand All') : localized('Collapse All')}
mode={RetinaImg.Mode.ContentIsMask}
/>
</div>
);
}
_renderPopoutToggle() {
if (AppEnv.isThreadWindow()) {
return (
<div onClick={this._onPopThreadIn}>
<RetinaImg
name="thread-popin.png"
title={localized('Pop thread in')}
mode={RetinaImg.Mode.ContentIsMask}
/>
</div>
);
}
return (
<div onClick={this._onPopoutThread}>
<RetinaImg
name="thread-popout.png"
title={localized('Popout thread')}
mode={RetinaImg.Mode.ContentIsMask}
<SubjectLineIcons
canCollapse={this.state.canCollapse}
hasCollapsedItems={this.state.hasCollapsedItems}
onPrint={this._onPrintThread}
onPopIn={this._onPopThreadIn}
onPopOut={this._onPopoutThread}
onToggleAllExpanded={this._onToggleAllMessagesExpanded}
/>
</div>
);
@ -500,15 +409,17 @@ class MessageList extends React.Component<{}, MessageListState> {
ready: !this.state.loading,
});
const messageListClass = classNames({
'message-list': true,
'height-fix': SearchableComponentStore.searchTerm !== null,
});
return (
<KeyCommandsRegion globalHandlers={this._globalKeymapHandlers()}>
<FindInThread />
<div className={messageListClass} id="message-list">
<div
id="message-list"
className={classNames({
'message-list': true,
'restrict-width': AppEnv.config.get(PREF_RESTRICT_WIDTH),
'height-fix': SearchableComponentStore.searchTerm !== null,
})}
>
<ScrollRegion
tabIndex={-1}
className={wrapClass}
@ -523,7 +434,10 @@ class MessageList extends React.Component<{}, MessageListState> {
<InjectedComponentSet
className="message-list-headers"
matching={{ role: 'MessageListHeaders' }}
exposedProps={{ thread: this.state.currentThread, messages: this.state.messages }}
exposedProps={{
thread: this.state.currentThread,
messages: this.state.messages,
}}
direction="column"
/>
</div>

View file

@ -0,0 +1,51 @@
import React from 'react';
import { RetinaImg } from 'mailspring-component-kit';
import { localized } from 'mailspring-exports';
interface SubjectLineIconsProps {
canCollapse: boolean;
hasCollapsedItems: boolean;
onPrint: () => void;
onPopIn: () => void;
onPopOut: () => void;
onToggleAllExpanded: () => void;
}
export const SubjectLineIcons: React.FunctionComponent<SubjectLineIconsProps> = props => (
<div className="message-icons-wrap">
{props.canCollapse && (
<div onClick={props.onToggleAllExpanded}>
<RetinaImg
name={props.hasCollapsedItems ? 'expand.png' : 'collapse.png'}
title={props.hasCollapsedItems ? localized('Expand All') : localized('Collapse All')}
mode={RetinaImg.Mode.ContentIsMask}
/>
</div>
)}
<div onClick={props.onPrint}>
<RetinaImg
name="print.png"
title={localized('Print Thread')}
mode={RetinaImg.Mode.ContentIsMask}
/>
</div>
{AppEnv.isThreadWindow() ? (
<div onClick={props.onPopIn}>
<RetinaImg
name="thread-popin.png"
title={localized('Pop thread in')}
mode={RetinaImg.Mode.ContentIsMask}
/>
</div>
) : (
<div onClick={props.onPopOut}>
<RetinaImg
name="thread-popout.png"
title={localized('Popout thread')}
mode={RetinaImg.Mode.ContentIsMask}
/>
</div>
)}
</div>
);

View file

@ -136,6 +136,30 @@ body.platform-win32 {
padding: 0;
order: 2;
&.restrict-width {
.message-subject-wrap {
max-width: @message-max-width;
}
.message-list-headers {
max-width: @message-max-width;
}
.message-item-wrap {
max-width: @message-max-width;
}
.msg-lines {
max-width: @message-max-width;
}
.message-item-area {
max-width: @message-max-width;
}
.footer-reply-area-wrap {
max-width: @message-max-width;
}
.footer-reply-area {
max-width: @message-max-width;
}
}
search-match,
.search-match {
background: @text-color-search-match;
@ -179,7 +203,6 @@ body.platform-win32 {
}
.message-subject-wrap {
max-width: @message-max-width;
margin: 5px auto 10px auto;
-webkit-user-select: text;
line-height: @font-size-large * 1.8;
@ -215,10 +238,11 @@ body.platform-win32 {
.thread-injected-mail-labels {
vertical-align: top;
}
.message-list-headers {
margin: 0 auto;
width: 100%;
max-width: @message-max-width;
display: block;
.participants {
@ -250,7 +274,7 @@ body.platform-win32 {
.message-item-wrap {
transition: height 0.1s;
position: relative;
max-width: @message-max-width;
margin: 0 auto;
.message-item-white-wrap {
@ -355,7 +379,6 @@ body.platform-win32 {
}
}
.msg-lines {
max-width: @message-max-width;
margin: 0 auto;
width: 100%;
margin-top: -13px;
@ -479,7 +502,7 @@ body.platform-win32 {
.message-item-area {
width: 100%;
max-width: @message-max-width;
margin: 0 auto;
padding: 0 20px @spacing-standard 20px;
@ -520,7 +543,6 @@ body.platform-win32 {
.footer-reply-area-wrap {
overflow: hidden;
max-width: @message-max-width;
margin: -3px auto 10px auto;
position: relative;
@ -543,7 +565,7 @@ body.platform-win32 {
.footer-reply-area {
width: 100%;
max-width: @message-max-width;
margin: 0 auto;
padding: 12px @spacing-standard * 1.5;
}

View file

@ -114,6 +114,11 @@ export default {
default: true,
title: localized('Automatically load images in viewed messages'),
},
restrictMaxWidth: {
type: 'boolean',
default: false,
title: localized('Restrict width of messages to maximize readability'),
},
backspaceDelete: {
type: 'boolean',
default: false,

View file

@ -85,8 +85,10 @@
body {
padding: 0;
margin: auto;
max-width: 840px;
-webkit-font-smoothing: antialiased;
&.restrict-width {
max-width: 840px;
}
}
a {