~exprez135/mailspring-libre

8c7d695cfaec60fccf476d06532c7b94d196a8d3 — Janosch Maier 3 years ago b404ae5
Colorize Accounts (#2240)

* Add configuration option to colorise accounts

* Add German translation for account color

* Fix bug when account is unset in list-tabular-item

* Add account color for "from" field in compose window

* Add color coding to the preferences account list

* Fix styling issue in compose window where account labels where unaligned if only one account had a color configured

* Add account color inside the swipeable element

* Partial Revert "Add configuration option to colorise accounts"

This partially reverts commit f8efccb0f23d9c51d68d30dcbbfd330239ed12f0.

* Ensure that account color is not displayed in thread-list when it is unset

* Add colorized accounts in sidebar and contacts view

* Add new type Style to be used throughout the colorization code

* Add todo note for ensuring that the account color updates correctly

* Remove style definition and use react CSSProperties instead

* Fix crash when opening a draft that was created in GMail

* Refactor accountColor to color

* Ensure dynamic change of the account color in the sidebar

* Add AccountColorBar react component

Co-authored-by: Janosch Maier <maierjaonsch@gmail.com>
M app/internal_packages/account-sidebar/lib/components/account-sidebar.tsx => app/internal_packages/account-sidebar/lib/components/account-sidebar.tsx +1 -1
@@ 3,7 3,7 @@ import { Utils, DOMUtils, Account, AccountStore } from 'mailspring-exports';
import { OutlineView, ScrollRegion, Flexbox } from 'mailspring-component-kit';
import AccountSwitcher from './account-switcher';
import SidebarStore from '../sidebar-store';
import { ISidebarSection, ISidebarItem } from '../types';
import { ISidebarSection } from '../types';

interface AccountSidebarState {
  accounts: Account[];

M app/internal_packages/account-sidebar/lib/sidebar-section.ts => app/internal_packages/account-sidebar/lib/sidebar-section.ts +3 -1
@@ 177,7 177,7 @@ class SidebarSection {
  }

  static forUserCategories(
    account,
    account: Account,
    { title, collapsible }: { title?: string; collapsible?: boolean } = {}
  ): ISidebarSection {
    let onCollapseToggled;


@@ 239,12 239,14 @@ class SidebarSection {
    if (collapsible) {
      onCollapseToggled = toggleSectionCollapsed;
    }
    const titleColor = account.color;

    return {
      title,
      iconName,
      items,
      collapsed,
      titleColor,
      onCollapseToggled,
      onItemCreated(displayName) {
        if (!displayName) {

M app/internal_packages/account-sidebar/lib/sidebar-store.ts => app/internal_packages/account-sidebar/lib/sidebar-store.ts +5 -5
@@ 15,7 15,7 @@ import SidebarSection from './sidebar-section';
import * as SidebarActions from './sidebar-actions';
import * as AccountCommands from './account-commands';
import { Disposable } from 'event-kit';
import { ISidebarItem, ISidebarSection } from './types';
import { ISidebarSection } from './types';

const Sections = {
  Standard: 'Standard',


@@ 27,9 27,9 @@ class SidebarStore extends MailspringStore {
    Standard: ISidebarSection;
    User: ISidebarSection[];
  } = {
    Standard: { title: '', items: [] },
    User: [],
  };
      Standard: { title: '', items: [] },
      User: [],
    };
  configSubscription: Disposable;

  constructor() {


@@ 151,7 151,7 @@ class SidebarStore extends MailspringStore {
    const multiAccount = accounts.length > 1;

    this._sections[Sections.Standard] = SidebarSection.standardSectionForAccounts(accounts);
    this._sections[Sections.User] = accounts.map(function(acc) {
    this._sections[Sections.User] = accounts.map(function (acc) {
      const opts: { title?: string; collapsible?: boolean } = {};
      if (multiAccount) {
        opts.title = acc.label;

M app/internal_packages/account-sidebar/lib/types.ts => app/internal_packages/account-sidebar/lib/types.ts +1 -0
@@ 27,6 27,7 @@ export interface ISidebarSection {
  items: ISidebarItem[];
  iconName?: string;
  collapsed?: boolean;
  titleColor?: string;
  onCollapseToggled?: () => void;
  onItemCreated?: (displayName) => void;
}

M app/internal_packages/composer/lib/account-contact-field.tsx => app/internal_packages/composer/lib/account-contact-field.tsx +39 -6
@@ 1,4 1,4 @@
import React from 'react';
import React, { CSSProperties } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import {


@@ 19,6 19,7 @@ interface AccountContactFieldProps {
  draft: Message;
  onChange: (val: { from: Contact[]; cc: Contact[]; bcc: Contact[] }) => void;
}

export default class AccountContactField extends React.Component<AccountContactFieldProps> {
  static displayName = 'AccountContactField';



@@ 58,6 59,18 @@ export default class AccountContactField extends React.Component<AccountContactF
    const label = this.props.value.toString();
    const multipleAccounts = this.props.accounts.length > 1;
    const hasAliases = this.props.accounts[0] && this.props.accounts[0].aliases.length > 0;
    const account = AccountStore.accountForEmail(this.props.value.email);
    let style: CSSProperties = {
    }
    if (account && account.color) {
      style = {
        ...style,
        borderLeftColor: account.color,
        paddingLeft: '8px',
        borderLeftWidth: '8px',
        borderLeftStyle: 'solid',
      }
    }

    if (multipleAccounts || hasAliases) {
      return (


@@ 66,28 79,48 @@ export default class AccountContactField extends React.Component<AccountContactF
            this._dropdownComponent = cm;
          }}
          bordered={false}
          primaryItem={<span>{label}</span>}
          primaryItem={<span style={style}>{label}</span>}
          menu={this._renderAccounts(this.props.accounts)}
        />
      );
    }
    return this._renderAccountSpan(label);
    return this._renderAccountSpan(label, style);
  }

  _renderAccountSpan = label => {
  _renderAccountSpan = (label, style) => {
    style = {
      ...style,
      position: 'relative',
      top: 13,
      left: '0.5em',
    }

    return (
      <span className="from-single-name" style={{ position: 'relative', top: 13, left: '0.5em' }}>
      <span className="from-single-name" style={style}>
        {label}
      </span>
    );
  };

  _renderMenuItem = contact => {
    const account = AccountStore.accountForId(contact.accountId)
    let style: CSSProperties = {}
    if (account && account.color) {
      style = {
        ...style,
        borderLeftColor: account.color,
        paddingLeft: '8px',
        borderLeftWidth: '8px',
        borderLeftStyle: 'solid',
      }
    }
    const className = classnames({
      contact: true,
      'is-alias': contact.isAlias,
    });
    return <span className={className}>{contact.toString()}</span>;
    return <div className={className} style={style}>
      {contact.toString()}
    </div>;
  };

  _renderAccounts(accounts) {

M app/internal_packages/contacts/lib/ContactList.tsx => app/internal_packages/contacts/lib/ContactList.tsx +16 -6
@@ 1,5 1,5 @@
import React from 'react';
import { Contact, localized, CanvasUtils } from 'mailspring-exports';
import React, { CSSProperties } from 'react';
import { Contact, localized, CanvasUtils, AccountStore } from 'mailspring-exports';
import {
  FocusContainer,
  MultiselectList,


@@ 16,9 16,20 @@ const ContactColumn = new ListTabular.Column({
  flex: 1,
  resolver: (contact: Contact) => {
    // until we revisit the UI to accommodate more icons
    const account = AccountStore.accountForId(contact.accountId);
    let style: CSSProperties = {}
    if (account && account.color) {
      style = {
        height: '50%',
        paddingLeft: '4px',
        borderLeftWidth: '4px',
        borderLeftColor: account.color,
        borderLeftStyle: 'solid',
      }
    }
    return (
      <div style={{ display: 'flex', alignItems: 'flex-start' }}>
        <div className="subject" dir="auto">
        <div style={style} className="subject" dir="auto">
          {contact.name}
        </div>
      </div>


@@ 109,9 120,8 @@ const ContactListSearchWithData = (props: ContactListSearchWithDataProps) => {
        type="text"
        ref={this._searchEl}
        value={props.search}
        placeholder={`${localized('Search')} ${
          props.perspective.type === 'unified' ? 'All Contacts' : props.perspective.label
        }`}
        placeholder={`${localized('Search')} ${props.perspective.type === 'unified' ? 'All Contacts' : props.perspective.label
          }`}
        onChange={e => props.setSearch(e.currentTarget.value)}
      />
      {props.search.length > 0 && (

M app/internal_packages/preferences/lib/tabs/preferences-account-details.tsx => app/internal_packages/preferences/lib/tabs/preferences-account-details.tsx +31 -4
@@ 57,7 57,7 @@ class PreferencesAccountDetails extends Component<
  {
    account: Account;
  }
> {
  > {
  static propTypes = {
    account: PropTypes.object,
    onAccountUpdated: PropTypes.func.isRequired,


@@ 107,7 107,7 @@ class PreferencesAccountDetails extends Component<
    this.props.onAccountUpdated(this.props.account, this.state.account);
  };

  _setState = (updates, callback = () => {}) => {
  _setState = (updates, callback = () => { }) => {
    const account = Object.assign(this.state.account.clone(), updates);
    this.setState({ account }, callback);
  };


@@ 169,6 169,21 @@ class PreferencesAccountDetails extends Component<
    ipcRenderer.send('command', 'application:show-contacts', {});
  };

  _onSetColor = (colorChanged) => {
    // TODO: Ensure that the account color is updated in all places where it is displayed:
    // - internal_packages/composer/lib/account-contict-field.tsx
    // - internal_packages/contacts/lib/ContactsList.tsx
    // - internal_packages/preferecnes/lib/preferences-account-list.tsx
    // - internal/packages/thread-list/lib/thread-lib-participants.tsx
    // - src/components/outline-view.tsx
    this._setState(colorChanged)
  }

  _onResetColor = () => {
    this.state.account.color = '';
    this._saveChanges();
  }

  _onContactSupport = () => {
    shell.openExternal('https://support.getmailspring.com/hc/en-us/requests/new');
  };


@@ 316,8 331,20 @@ class PreferencesAccountDetails extends Component<
            </select>
          </div>
        ) : (
          undefined
        )}
            undefined
          )}
        <h6>{localized('Account Color')}</h6>
        <div style={{ display: 'flex', alignItems: 'flex-end' }}>
          <input
            type="color"
            value={account.color}
            onBlur={this._saveChanges}
            onChange={e => this._onSetColor({ color: e.target.value })}
          />
          <div className="btn" style={{ marginLeft: 6 }} onClick={this._onResetColor}>
            {localized('Reset Account Color')}
          </div>
        </div>
        <h6>{localized('Account Settings')}</h6>
        <div className="btn" onClick={this._onManageContacts}>
          {localized('Manage Contacts')}

M app/internal_packages/preferences/lib/tabs/preferences-account-list.tsx => app/internal_packages/preferences/lib/tabs/preferences-account-list.tsx +8 -2
@@ 1,4 1,4 @@
import React, { Component } from 'react';
import React, { Component, CSSProperties } from 'react';
import { localized, Account } from 'mailspring-exports';
import { RetinaImg, Flexbox, EditableList } from 'mailspring-component-kit';
import classnames from 'classnames';


@@ 42,9 42,15 @@ class PreferencesAccountList extends Component<PreferencesAccountListProps> {
    const label = account.label;
    const accountSub = `${account.name || localized('No name provided')} <${account.emailAddress}>`;
    const syncError = account.hasSyncStateError();
    let style: CSSProperties = {}
    if (account.color) {
      style = { borderLeftColor: account.color, borderLeftWidth: '8px', borderLeftStyle: 'solid' }
    } else {
      style = { marginLeft: '8px' }
    }

    return (
      <div className={classnames({ account: true, 'sync-error': syncError })} key={account.id}>
      <div style={style} className={classnames({ account: true, 'sync-error': syncError })} key={account.id}>
        <Flexbox direction="row" style={{ alignItems: 'middle' }}>
          <div style={{ textAlign: 'center' }}>
            <RetinaImg

M app/internal_packages/thread-list/lib/thread-list-participants.tsx => app/internal_packages/thread-list/lib/thread-list-participants.tsx +5 -3
@@ 1,5 1,6 @@
import React from 'react';
import { PropTypes, Utils } from 'mailspring-exports';
import { AccountColorBar } from 'mailspring-component-kit';
import { ThreadWithMessagesMetadata } from './types';

class ThreadListParticipants extends React.Component<{ thread: ThreadWithMessagesMetadata }> {


@@ 18,6 19,7 @@ class ThreadListParticipants extends React.Component<{ thread: ThreadWithMessage
    const items = this.getTokens();
    return (
      <div className="participants" dir="auto">
        <AccountColorBar accountId={this.props.thread.accountId} />
        {this.renderSpans(items)}
      </div>
    );


@@ 28,7 30,7 @@ class ThreadListParticipants extends React.Component<{ thread: ThreadWithMessage
    let accumulated = null;
    let accumulatedUnread = false;

    const flush = function() {
    const flush = function () {
      if (accumulated) {
        spans.push(
          <span key={spans.length} className={`unread-${accumulatedUnread}`}>


@@ 40,7 42,7 @@ class ThreadListParticipants extends React.Component<{ thread: ThreadWithMessage
      accumulatedUnread = false;
    };

    const accumulate = function(text, unread?: boolean) {
    const accumulate = function (text, unread?: boolean) {
      if (accumulated && unread && accumulatedUnread !== unread) {
        flush();
      }


@@ 147,7 149,7 @@ class ThreadListParticipants extends React.Component<{ thread: ThreadWithMessage
    if (
      list.length === 0 &&
      (this.props.thread.participants != null ? this.props.thread.participants.length : undefined) >
        0
      0
    ) {
      list.push({ contact: this.props.thread.participants[0], unread: false });
    }

M app/lang/de.json => app/lang/de.json +3 -0
@@ 17,6 17,7 @@
  "About Mailspring": "Über Mailspring",
  "Accept": "Annehmen",
  "Account": "Konto",
  "Account Color": "Kontofarbe",
  "Account Details": "Accountdetails",
  "Account Label": "Konto-Label",
  "Account Settings": "Konten-Einstellungen",


@@ 364,6 365,7 @@
  "Make sure you have `libsecret` installed and a keyring is present. ": "Stellen Sie sicher, dass `libsecret` installiert ist und ein Schlüsselring vorhanden ist.",
  "Manage": "Verwalten",
  "Manage Accounts": "Konten verwalten",
  "Manage Contacts": "Kontakte verwalten",
  "Manage Billing": "Bezahlung verwalten",
  "Manage Templates...": "Vorlagen verwalten...",
  "Manually": "Manuell",


@@ 537,6 539,7 @@
  "Reply Rate": "Antwortquote",
  "Reply to": "Antwort an",
  "Reset": "Zurücksetzen",
  "Reset Account Color": "Kontofarbe zurücksetzen",
  "Reset Accounts and Settings": "Konten und Einstellungen zurücksetzen",
  "Reset Cache": "Cache zurücksetzen",
  "Reset Configuration": "Konfiguration zurücksetzen",

A app/src/components/account-color-bar.tsx => app/src/components/account-color-bar.tsx +49 -0
@@ 0,0 1,49 @@
import { AccountStore } from 'mailspring-exports';
import React from 'react';

class AccountColorBar extends React.Component<{ accountId: string }, { color: string | null }> {

    static displayName = 'AccountColorBar';

    unsubscribe?: () => void;

    constructor(props) {
        super(props);
        this.state = { color: this.getColor(props) };
    }

    getColor = (props = this.props) => {
        const account = AccountStore.accountForId(props.accountId);
        return account ? account.color : null;
    }

    componentDidMount() {
        this.unsubscribe = AccountStore.listen(() => {
            const nextColor = this.getColor();
            if (this.state.color !== nextColor) this.setState({ color: nextColor });
        });
    }

    componentWillUnmount() {
        this.unsubscribe && this.unsubscribe();
    }

    render() {
        return this.state.color ? (
            <span
                style={{
                    height: '50%',
                    paddingLeft: '4px',
                    borderLeftWidth: '4px',
                    borderLeftColor: this.state.color,
                    borderLeftStyle: 'solid',
                }}
            />
        ) : (
                <span />
            );
    }

}

export default AccountColorBar;
\ No newline at end of file

M app/src/components/list-tabular-item.tsx => app/src/components/list-tabular-item.tsx +1 -2
@@ 86,8 86,7 @@ export default class ListTabularItem extends React.Component<ListTabularItemProp
    return (this.props.columns || []).map(column => {
      if (names[column.name]) {
        console.warn(
          `ListTabular: Columns do not have distinct names, will cause React error! \`${
            column.name
          `ListTabular: Columns do not have distinct names, will cause React error! \`${column.name
          }\` twice.`
        );
      }

M app/src/components/outline-view.tsx => app/src/components/outline-view.tsx +15 -2
@@ 1,5 1,5 @@
import { Utils, localized } from 'mailspring-exports';
import React, { Component } from 'react';
import React, { Component, CSSProperties } from 'react';
import { DropZone } from './drop-zone';
import { RetinaImg } from './retina-img';
import OutlineViewItem from './outline-view-item';


@@ 34,6 34,7 @@ interface OutlineViewProps {
  items: IOutlineViewItem[];
  iconName?: string;
  collapsed?: boolean;
  titleColor?: string;
  onCollapseToggled?: (props: OutlineViewProps) => void;
  onItemCreated?: (displayName) => void;
}


@@ 63,6 64,7 @@ interface OutlineViewState {
 * OutlineViewItem}s
 * @param {boolean} props.collapsed - Whether the OutlineView is collapsed or
 * not
 * @param {string} props.titleColor - Colored bar that is displayed to highlight the title
 * @param {props.onItemCreated} props.onItemCreated
 * @param {props.onCollapseToggled} props.onCollapseToggled
 * @class OutlineView


@@ 83,6 85,7 @@ export class OutlineView extends Component<OutlineViewProps, OutlineViewState> {
   */
  static propTypes = {
    title: PropTypes.string,
    titleColor: PropTypes.string,
    iconName: PropTypes.string,
    items: PropTypes.array,
    collapsed: PropTypes.bool,


@@ 181,6 184,16 @@ export class OutlineView extends Component<OutlineViewProps, OutlineViewState> {

  _renderHeading(allowCreate, collapsed, collapsible) {
    const collapseLabel = collapsed ? localized('Show') : localized('Hide');
    let style: CSSProperties = {}
    if (this.props.titleColor) {
      style = {
        height: '50%',
        paddingLeft: '4px',
        borderLeftWidth: '4px',
        borderLeftColor: this.props.titleColor,
        borderLeftStyle: 'solid',
      }
    }
    return (
      <DropZone
        className="heading"


@@ 188,7 201,7 @@ export class OutlineView extends Component<OutlineViewProps, OutlineViewState> {
        onDragStateChange={this._onDragStateChange}
        shouldAcceptDrop={() => true}
      >
        <span className="text" title={this.props.title}>
        <span style={style} className="text" title={this.props.title}>
          {this.props.title}
        </span>
        {allowCreate ? this._renderCreateButton() : null}

M app/src/flux/models/account.ts => app/src/flux/models/account.ts +6 -0
@@ 81,6 81,10 @@ export class Account extends ModelWithMetadata {
    authedAt: Attributes.DateTime({
      modelKey: 'authedAt',
    }),

    color: Attributes.String({
      modelKey: 'color',
    }),
  };

  public name: string;


@@ 108,6 112,7 @@ export class Account extends ModelWithMetadata {
  public defaultAlias: string;
  public syncState: string;
  public syncError: string;
  public color: string;

  constructor(args) {
    super(args);


@@ 119,6 124,7 @@ export class Account extends ModelWithMetadata {
      type: 'bcc',
      value: '',
    };
    this.color = this.color || '';
  }

  toJSON() {

M app/src/global/mailspring-component-kit.d.ts => app/src/global/mailspring-component-kit.d.ts +1 -0
@@ 65,6 65,7 @@ export * from '../components/scroll-region';
export * from '../components/resizable-region';
export * from '../components/mail-label';

export const AccountColorBar: typeof import('../components/account-color-bar').default;
export const MailLabelSet: typeof import('../components/mail-label-set').default;
export const MailImportantIcon: typeof import('../components/mail-important-icon').default;


M app/src/global/mailspring-component-kit.js => app/src/global/mailspring-component-kit.js +1 -0
@@ 123,6 123,7 @@ lazyLoad('ComposerSupport', 'composer-editor/composer-support');
lazyLoad('ScrollRegion', 'scroll-region');
lazyLoad('ResizableRegion', 'resizable-region');

lazyLoad('AccountColorBar', 'account-color-bar');
lazyLoadFrom('MailLabel', 'mail-label');
lazyLoadFrom('LabelColorizer', 'mail-label');
lazyLoad('MailLabelSet', 'mail-label-set');