mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-26 22:47:50 +08:00
feat(account-sidebar): Ability to add custom items to the sidebar
This commit is contained in:
parent
3d84c41625
commit
a316518326
7 changed files with 217 additions and 155 deletions
|
@ -44,11 +44,8 @@ class AccountSidebarStore extends NylasStore
|
|||
account = AccountStore.current()
|
||||
return unless account
|
||||
|
||||
viewFilterForCategory = (cat) ->
|
||||
return MailViewFilter.forCategory(cat)
|
||||
|
||||
userCategories = CategoryStore.getUserCategories()
|
||||
userCategoryViews = _.map(userCategories, viewFilterForCategory)
|
||||
userCategoryItems = _.map(userCategories, @_sidebarItemForCategory)
|
||||
|
||||
# Our drafts are displayed via the `DraftListSidebarItem` which
|
||||
# is loading into the `Drafts` Sheet.
|
||||
|
@ -56,30 +53,33 @@ class AccountSidebarStore extends NylasStore
|
|||
standardCategories = _.reject standardCategories, (category) =>
|
||||
category.name is "drafts"
|
||||
|
||||
standardViews = _.map(standardCategories, viewFilterForCategory)
|
||||
|
||||
starredView = MailViewFilter.forStarred()
|
||||
standardViews.splice(1, 0, starredView)
|
||||
standardCategoryItems = _.map(standardCategories, @_sidebarItemForCategory)
|
||||
starredItem = @_sidebarItemForMailView('starred', MailViewFilter.forStarred())
|
||||
|
||||
# Find root views and add them to the bottom of the list (Drafts, etc.)
|
||||
_.each WorkspaceStore.Sheet, (sheet) ->
|
||||
if sheet.root and sheet.name
|
||||
standardViews.push(sheet)
|
||||
standardItems = standardCategoryItems
|
||||
standardItems.splice(1, 0, starredItem)
|
||||
standardItems.push(WorkspaceStore.sidebarItems()...)
|
||||
|
||||
@_sections = []
|
||||
@_sections.push
|
||||
label: 'Mailboxes'
|
||||
items: standardViews
|
||||
items: standardItems
|
||||
type: 'mailboxes'
|
||||
|
||||
@_sections.push
|
||||
label: CategoryStore.categoryLabel()
|
||||
items: userCategoryViews
|
||||
items: userCategoryItems
|
||||
type: 'category'
|
||||
|
||||
@trigger()
|
||||
|
||||
_isStandardCategory: (category) =>
|
||||
category.name and category.name in CategoryStore.standardCategories
|
||||
_sidebarItemForMailView: (id, filter) =>
|
||||
new WorkspaceStore.SidebarItem({id: id, name: filter.name, mailViewFilter: filter})
|
||||
|
||||
_sidebarItemForCategory: (category) =>
|
||||
filter = MailViewFilter.forCategory(category)
|
||||
@_sidebarItemForMailView(category.id, filter)
|
||||
|
||||
|
||||
module.exports = new AccountSidebarStore()
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
React = require 'react'
|
||||
{Actions, MailViewFilter, AccountStore} = require("nylas-exports")
|
||||
{Actions, MailViewFilter, WorkspaceStore} = require("nylas-exports")
|
||||
{ScrollRegion} = require("nylas-component-kit")
|
||||
SidebarDividerItem = require("./account-sidebar-divider-item")
|
||||
SidebarSheetItem = require("./account-sidebar-sheet-item")
|
||||
AccountSwitcher = require ("./account-switcher")
|
||||
AccountSidebarStore = require ("./account-sidebar-store")
|
||||
AccountSidebarMailViewItem = require("./account-sidebar-mail-view-item")
|
||||
crypto = require 'crypto'
|
||||
{RetinaImg} = require 'nylas-component-kit'
|
||||
classNames = require 'classnames'
|
||||
|
||||
class AccountSidebar extends React.Component
|
||||
@displayName: 'AccountSidebar'
|
||||
|
@ -19,169 +18,64 @@ class AccountSidebar extends React.Component
|
|||
|
||||
constructor: (@props) ->
|
||||
@state = @_getStateFromStores()
|
||||
@state.showing = false
|
||||
|
||||
componentDidMount: =>
|
||||
@unsubscribers = []
|
||||
@unsubscribers.push AccountSidebarStore.listen @_onStoreChange
|
||||
@unsubscribers.push AccountStore.listen @_onStoreChange
|
||||
|
||||
# It's important that every React class explicitly stops listening to
|
||||
# atom events before it unmounts. Thank you event-kit
|
||||
# This can be fixed via a Reflux mixin
|
||||
componentWillUnmount: =>
|
||||
unsubscribe() for unsubscribe in @unsubscribers
|
||||
|
||||
render: =>
|
||||
<ScrollRegion style={flex:1} id="account-sidebar">
|
||||
{@_accountSwitcher()}
|
||||
<AccountSwitcher />
|
||||
<div className="account-sidebar-sections">
|
||||
{@_sections()}
|
||||
</div>
|
||||
</ScrollRegion>
|
||||
|
||||
_accountSwitcher: =>
|
||||
return undefined if @state.accounts.length < 1
|
||||
|
||||
<div id="account-switcher" tabIndex={-1} onBlur={@_onBlur} ref="button">
|
||||
{@_renderAccount @state.account, true}
|
||||
{@_renderDropdown()}
|
||||
</div>
|
||||
|
||||
_renderAccount: (account, isPrimaryItem) =>
|
||||
classes = classNames
|
||||
"account": true
|
||||
"item": true
|
||||
"dropdown-item-padding": not isPrimaryItem
|
||||
"active": account is @state.account
|
||||
"bg-color-hover": not isPrimaryItem
|
||||
"primary-item": isPrimaryItem
|
||||
"account-option": not isPrimaryItem
|
||||
|
||||
email = account.emailAddress.trim().toLowerCase()
|
||||
|
||||
if isPrimaryItem
|
||||
dropdownClasses = classNames
|
||||
"account-switcher-dropdown": true,
|
||||
"account-switcher-dropdown-hidden": @state.showing
|
||||
|
||||
dropdownArrow = <div style={float: 'right', marginTop: -2}>
|
||||
<RetinaImg className={dropdownClasses} name="account-switcher-dropdown.png"
|
||||
mode={RetinaImg.Mode.ContentPreserve} />
|
||||
</div>
|
||||
|
||||
onClick = @_toggleDropdown
|
||||
|
||||
else
|
||||
onClick = =>
|
||||
@_onSwitchAccount account
|
||||
|
||||
<div className={classes}
|
||||
onClick={onClick}
|
||||
key={email}>
|
||||
<div style={float: 'left'}>
|
||||
<div className="gravatar" style={backgroundImage: @_gravatarUrl(email)}></div>
|
||||
<RetinaImg name={"ic-settings-account-#{account.provider}@2x.png"}
|
||||
style={width: 28, height: 28, marginTop: -10}
|
||||
fallback="ic-settings-account-imap.png"
|
||||
mode={RetinaImg.Mode.ContentPreserve} />
|
||||
</div>
|
||||
{dropdownArrow}
|
||||
<div className="name" style={lineHeight: "110%"}>
|
||||
{email}
|
||||
</div>
|
||||
<div style={clear: "both"}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
_renderNewAccountOption: =>
|
||||
<div className="account item dropdown-item-padding bg-color-hover new-account-option"
|
||||
onClick={@_onAddAccount}
|
||||
tabIndex={999}>
|
||||
<div style={float: 'left'}>
|
||||
<RetinaImg name="icon-accounts-addnew.png"
|
||||
fallback="ic-settings-account-imap.png"
|
||||
mode={RetinaImg.Mode.ContentPreserve}
|
||||
style={width: 28, height: 28, marginTop: -10} />
|
||||
</div>
|
||||
<div className="name" style={lineHeight: "110%", textTransform: 'none'}>
|
||||
Add account…
|
||||
</div>
|
||||
<div style={clear: "both"}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
_renderDropdown: =>
|
||||
display = if @state.showing then "block" else "none"
|
||||
# display = "block"
|
||||
|
||||
accounts = @state.accounts.map (a) =>
|
||||
@_renderAccount(a)
|
||||
|
||||
<div style={display: display}
|
||||
ref="account-switcher-dropdown"
|
||||
className="dropdown dropdown-positioning dropdown-colors">
|
||||
{accounts}
|
||||
{@_renderNewAccountOption()}
|
||||
</div>
|
||||
|
||||
_toggleDropdown: =>
|
||||
@setState showing: !@state.showing
|
||||
|
||||
_gravatarUrl: (email) =>
|
||||
hash = crypto.createHash('md5').update(email, 'utf8').digest('hex')
|
||||
|
||||
"url(http://www.gravatar.com/avatar/#{hash}?d=blank&s=56)"
|
||||
|
||||
_sections: =>
|
||||
return @state.sections.map (section) =>
|
||||
@state.sections.map (section) =>
|
||||
<section key={section.label}>
|
||||
<div className="heading">{section.label}</div>
|
||||
{@_itemComponents(section)}
|
||||
</section>
|
||||
|
||||
_itemComponents: (section) =>
|
||||
section.items?.map (item) =>
|
||||
return unless item
|
||||
if item instanceof MailViewFilter
|
||||
<AccountSidebarMailViewItem
|
||||
key={item.id ? item.type}
|
||||
mailView={item}
|
||||
select={ item.isEqual(@state.selected) }/>
|
||||
else
|
||||
if item.sidebarComponent
|
||||
itemClass = item.sidebarComponent
|
||||
else
|
||||
itemClass = SidebarSheetItem
|
||||
section.items.map (item) =>
|
||||
unless item instanceof WorkspaceStore.SidebarItem
|
||||
throw new Error("AccountSidebar:_itemComponents: sections contained an \
|
||||
item which was not a SidebarItem")
|
||||
|
||||
<itemClass
|
||||
key={item.id ? item.type}
|
||||
if item.component
|
||||
Component = item.component
|
||||
<Component
|
||||
key={item.id}
|
||||
item={item}
|
||||
sectionType={section.type}
|
||||
select={item.id is @state.selected?.id }/>
|
||||
select={item.id is @state.selected?.id } />
|
||||
|
||||
else if item.mailViewFilter
|
||||
<AccountSidebarMailViewItem
|
||||
key={item.id}
|
||||
mailView={item.mailViewFilter}
|
||||
select={item.mailViewFilter.isEqual(@state.selected)} />
|
||||
|
||||
else if item.sheet
|
||||
<SidebarSheetItem
|
||||
key={item.id}
|
||||
item={item.sheet}
|
||||
select={item.sheet.id is @state.selected?.id} />
|
||||
|
||||
else
|
||||
throw new Error("AccountSidebar:_itemComponents: each item must have a \
|
||||
custom component, or a sheet or mailViewFilter")
|
||||
|
||||
_onStoreChange: =>
|
||||
@setState @_getStateFromStores()
|
||||
|
||||
_onBlur: (e) =>
|
||||
target = e.nativeEvent.relatedTarget
|
||||
if target? and React.findDOMNode(@refs.button).contains(target)
|
||||
return
|
||||
@setState(showing: false)
|
||||
|
||||
_onSwitchAccount: (account) =>
|
||||
Actions.selectAccountId(account.id)
|
||||
@setState(showing: false)
|
||||
|
||||
_onAddAccount: =>
|
||||
require('remote').getGlobal('application').windowManager.newOnboardingWindow()
|
||||
@setState showing: false
|
||||
|
||||
_getStateFromStores: =>
|
||||
sections: AccountSidebarStore.sections()
|
||||
selected: AccountSidebarStore.selected()
|
||||
accounts: AccountStore.items()
|
||||
account: AccountStore.current()
|
||||
|
||||
|
||||
module.exports = AccountSidebar
|
||||
|
|
136
internal_packages/account-sidebar/lib/account-switcher.cjsx
Normal file
136
internal_packages/account-sidebar/lib/account-switcher.cjsx
Normal file
|
@ -0,0 +1,136 @@
|
|||
React = require 'react'
|
||||
{Actions, AccountStore} = require("nylas-exports")
|
||||
{ScrollRegion} = require("nylas-component-kit")
|
||||
crypto = require 'crypto'
|
||||
{RetinaImg} = require 'nylas-component-kit'
|
||||
classNames = require 'classnames'
|
||||
|
||||
class AccountSwitcher extends React.Component
|
||||
@displayName: 'AccountSwitcher'
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = @_getStateFromStores()
|
||||
@state.showing = false
|
||||
|
||||
componentDidMount: =>
|
||||
@unsubscribers = []
|
||||
@unsubscribers.push AccountStore.listen @_onStoreChange
|
||||
|
||||
componentWillUnmount: =>
|
||||
unsubscribe() for unsubscribe in @unsubscribers
|
||||
|
||||
render: =>
|
||||
return undefined if @state.accounts.length < 1
|
||||
|
||||
<div id="account-switcher" tabIndex={-1} onBlur={@_onBlur} ref="button">
|
||||
{@_renderAccount(@state.account, true)}
|
||||
{@_renderDropdown()}
|
||||
</div>
|
||||
|
||||
_renderAccount: (account, isPrimaryItem) =>
|
||||
classes = classNames
|
||||
"account": true
|
||||
"item": true
|
||||
"dropdown-item-padding": not isPrimaryItem
|
||||
"active": account is @state.account
|
||||
"bg-color-hover": not isPrimaryItem
|
||||
"primary-item": isPrimaryItem
|
||||
"account-option": not isPrimaryItem
|
||||
|
||||
email = account.emailAddress.trim().toLowerCase()
|
||||
|
||||
if isPrimaryItem
|
||||
dropdownClasses = classNames
|
||||
"account-switcher-dropdown": true,
|
||||
"account-switcher-dropdown-hidden": @state.showing
|
||||
|
||||
dropdownArrow = <div style={float: 'right', marginTop: -2}>
|
||||
<RetinaImg className={dropdownClasses} name="account-switcher-dropdown.png"
|
||||
mode={RetinaImg.Mode.ContentPreserve} />
|
||||
</div>
|
||||
|
||||
onClick = @_toggleDropdown
|
||||
|
||||
else
|
||||
onClick = =>
|
||||
@_onSwitchAccount account
|
||||
|
||||
<div className={classes}
|
||||
onClick={onClick}
|
||||
key={email}>
|
||||
<div style={float: 'left'}>
|
||||
<div className="gravatar" style={backgroundImage: @_gravatarUrl(email)}></div>
|
||||
<RetinaImg name={"ic-settings-account-#{account.provider}@2x.png"}
|
||||
style={width: 28, height: 28, marginTop: -10}
|
||||
fallback="ic-settings-account-imap.png"
|
||||
mode={RetinaImg.Mode.ContentPreserve} />
|
||||
</div>
|
||||
{dropdownArrow}
|
||||
<div className="name" style={lineHeight: "110%"}>
|
||||
{email}
|
||||
</div>
|
||||
<div style={clear: "both"}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
_renderNewAccountOption: =>
|
||||
<div className="account item dropdown-item-padding bg-color-hover new-account-option"
|
||||
onClick={@_onAddAccount}
|
||||
tabIndex={999}>
|
||||
<div style={float: 'left'}>
|
||||
<RetinaImg name="icon-accounts-addnew.png"
|
||||
fallback="ic-settings-account-imap.png"
|
||||
mode={RetinaImg.Mode.ContentPreserve}
|
||||
style={width: 28, height: 28, marginTop: -10} />
|
||||
</div>
|
||||
<div className="name" style={lineHeight: "110%", textTransform: 'none'}>
|
||||
Add account…
|
||||
</div>
|
||||
<div style={clear: "both"}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
_renderDropdown: =>
|
||||
display = if @state.showing then "block" else "none"
|
||||
# display = "block"
|
||||
|
||||
accounts = @state.accounts.map (a) =>
|
||||
@_renderAccount(a)
|
||||
|
||||
<div style={display: display}
|
||||
ref="account-switcher-dropdown"
|
||||
className="dropdown dropdown-positioning dropdown-colors">
|
||||
{accounts}
|
||||
{@_renderNewAccountOption()}
|
||||
</div>
|
||||
|
||||
_toggleDropdown: =>
|
||||
@setState showing: !@state.showing
|
||||
|
||||
_gravatarUrl: (email) =>
|
||||
hash = crypto.createHash('md5').update(email, 'utf8').digest('hex')
|
||||
"url(http://www.gravatar.com/avatar/#{hash}?d=blank&s=56)"
|
||||
|
||||
_onStoreChange: =>
|
||||
@setState @_getStateFromStores()
|
||||
|
||||
_onBlur: (e) =>
|
||||
target = e.nativeEvent.relatedTarget
|
||||
if target? and React.findDOMNode(@refs.button).contains(target)
|
||||
return
|
||||
@setState(showing: false)
|
||||
|
||||
_onSwitchAccount: (account) =>
|
||||
Actions.selectAccountId(account.id)
|
||||
@setState(showing: false)
|
||||
|
||||
_onAddAccount: =>
|
||||
require('remote').getGlobal('application').windowManager.newOnboardingWindow()
|
||||
@setState showing: false
|
||||
|
||||
_getStateFromStores: =>
|
||||
accounts: AccountStore.items()
|
||||
account: AccountStore.current()
|
||||
|
||||
|
||||
module.exports = AccountSwitcher
|
|
@ -7,7 +7,7 @@ SettingsTabsView = require "./settings-tabs-view"
|
|||
module.exports =
|
||||
|
||||
activate: (@state={}) ->
|
||||
WorkspaceStore.defineSheet 'Settings', {root: true, supportedModes: ['list'], name: 'Plugins'},
|
||||
WorkspaceStore.defineSheet 'Settings', {root: true, supportedModes: ['list']},
|
||||
list: ['RootSidebar', 'SettingsSidebar', 'Settings']
|
||||
|
||||
ComponentRegistry.register SettingsTabsView,
|
||||
|
|
|
@ -34,11 +34,11 @@ class DraftListSidebarItem extends React.Component
|
|||
<div className={classSet} onClick={@_onClick}>
|
||||
{unread}
|
||||
<div className="icon"><RetinaImg name={'drafts.png'} mode={RetinaImg.Mode.ContentIsMask} /></div>
|
||||
<div className="name"> {@props.item.name}</div>
|
||||
<div className="name"> Drafts</div>
|
||||
</div>
|
||||
|
||||
_onClick: (event) =>
|
||||
event.preventDefault()
|
||||
Actions.selectRootSheet(@props.item)
|
||||
Actions.selectRootSheet(WorkspaceStore.Sheet.Drafts)
|
||||
|
||||
module.exports = DraftListSidebarItem
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
_ = require 'underscore'
|
||||
React = require "react"
|
||||
{ComponentRegistry, WorkspaceStore} = require "nylas-exports"
|
||||
{MailViewFilter, ComponentRegistry, WorkspaceStore} = require "nylas-exports"
|
||||
|
||||
{DownButton, UpButton, ThreadBulkArchiveButton, ThreadBulkStarButton, ThreadBulkToggleUnreadButton} = require "./thread-buttons"
|
||||
{DraftDeleteButton} = require "./draft-buttons"
|
||||
|
@ -13,9 +13,17 @@ DraftList = require './draft-list'
|
|||
|
||||
module.exports =
|
||||
activate: (@state={}) ->
|
||||
WorkspaceStore.defineSheet 'Drafts', {root: true, name: 'Drafts', sidebarComponent: DraftListSidebarItem},
|
||||
WorkspaceStore.defineSheet 'Drafts', {root: true},
|
||||
list: ['RootSidebar', 'DraftList']
|
||||
|
||||
@sidebarItem = new WorkspaceStore.SidebarItem
|
||||
component: DraftListSidebarItem
|
||||
sheet: WorkspaceStore.Sheet.Drafts
|
||||
id: 'Drafts'
|
||||
name: 'Drafts'
|
||||
|
||||
WorkspaceStore.addSidebarItem(@sidebarItem)
|
||||
|
||||
ComponentRegistry.register ThreadList,
|
||||
location: WorkspaceStore.Location.ThreadList
|
||||
|
||||
|
|
|
@ -5,6 +5,13 @@ NylasStore = require 'nylas-store'
|
|||
|
||||
Sheet = {}
|
||||
Location = {}
|
||||
SidebarItems = {}
|
||||
|
||||
class WorkspaceSidebarItem
|
||||
constructor: ({@id, @component, @name, @sheet, @mailViewFilter}) ->
|
||||
if not @sheet and not @mailViewFilter and not @component
|
||||
throw new Error("WorkspaceSidebarItem: You must provide either a sheet \
|
||||
component, or a mailViewFilter for the sidebar item named #{@name}")
|
||||
|
||||
###
|
||||
Public: The WorkspaceStore manages Sheets and layout modes in the application.
|
||||
|
@ -40,6 +47,9 @@ class WorkspaceStore extends NylasStore
|
|||
@Location = Location = {}
|
||||
@Sheet = Sheet = {}
|
||||
|
||||
@SidebarItem = WorkspaceSidebarItem
|
||||
@SidebarItems = SidebarItems = {}
|
||||
|
||||
@_hiddenLocations = {}
|
||||
@_sheetStack = []
|
||||
|
||||
|
@ -137,6 +147,20 @@ class WorkspaceStore extends NylasStore
|
|||
return false unless loc
|
||||
@_hiddenLocations[loc.id]?
|
||||
|
||||
|
||||
sidebarItems: =>
|
||||
_.values(@SidebarItems)
|
||||
|
||||
addSidebarItem: (item) =>
|
||||
unless item instanceof WorkspaceSidebarItem
|
||||
throw new Error("WorkspaceStore::addSidebarItem requires a `WorkspaceSidebarItem`")
|
||||
@SidebarItems[item.id] = item
|
||||
@triggerDebounced()
|
||||
|
||||
removeSidebarItem: (item) =>
|
||||
delete @SidebarItems[item.id]
|
||||
@triggerDebounced()
|
||||
|
||||
###
|
||||
Managing Sheets
|
||||
###
|
||||
|
|
Loading…
Add table
Reference in a new issue