feat(menu): add right click menu

Summary:
Adds a right click menu to the thread list
Moving to folders and labels #TODO

Test Plan: TODO

Reviewers: bengotow, juan

Reviewed By: bengotow

Differential Revision: https://phab.nylas.com/D2609
This commit is contained in:
Evan Morikawa 2016-02-19 18:35:48 -05:00
parent 0559e92dc3
commit 3b8cc984d0
3 changed files with 193 additions and 13 deletions

View file

@ -0,0 +1,162 @@
import _ from 'underscore'
import {
Thread,
Actions,
Message,
TaskFactory,
DatabaseStore,
FocusedPerspectiveStore} from 'nylas-exports'
export default class ThreadListContextMenu {
constructor({threadIds = [], accountIds = []}) {
this.threadIds = threadIds
this.accountIds = accountIds
}
menuItemTemplate() {
return DatabaseStore.modelify(Thread, this.threadIds)
.then((threads) => {
this.threads = threads;
return Promise.all([
this.replyItem(),
this.replyAllItem(),
this.forwardItem(),
{type: 'separator'},
this.archiveItem(),
this.trashItem(),
this.markAsReadItem(),
this.starItem(),
// this.moveToOrLabelItem(),
// {type: 'separator'},
// this.extensionItems(),
])
}).then((menuItems) => {
return _.filter(_.compact(menuItems), (item, index) => {
if ((index === 0 || index === menuItems.length - 1) && item.type === "separator") {
return false
}
return true
});
});
}
replyItem() {
if (this.threadIds.length !== 1) { return null }
return {
label: "Reply",
click: () => {
Actions.composeReply({threadId: this.threadIds[0], popout: true});
},
}
}
replyAllItem() {
if (this.threadIds.length !== 1) { return null }
DatabaseStore.findBy(Message, {threadId: this.threadIds[0]})
.order(Message.attributes.date.descending())
.limit(1)
.then((message) => {
if (message && message.canReplyAll()) {
return {
label: "Reply All",
click: () => {
Actions.composeReplyAll({threadId: this.threadIds[0], popout: true});
},
}
}
return null
})
}
forwardItem() {
if (this.threadIds.length !== 1) { return null }
return {
label: "Forward",
click: () => {
Actions.composeForward({threadId: this.threadIds[0], popout: true});
},
}
}
archiveItem() {
if (!FocusedPerspectiveStore.current().canArchiveThreads()) {
return null
}
return {
label: "Archive",
click: () => {
const tasks = TaskFactory.tasksForArchiving({
threads: this.threads,
fromPerspective: FocusedPerspectiveStore.current(),
})
Actions.queueTasks(tasks)
},
}
}
trashItem() {
if (!FocusedPerspectiveStore.current().canTrashThreads()) {
return null
}
return {
label: "Trash",
click: () => {
const tasks = TaskFactory.tasksForMovingToTrash({
threads: this.threads,
fromPerspective: FocusedPerspectiveStore.current(),
})
Actions.queueTasks(tasks)
},
}
}
markAsReadItem() {
const unread = _.every(this.threads, (t) => {
return _.isMatch(t, {unread: false})
});
const dir = unread ? "Unread" : "Read"
return {
label: `Mark as ${dir}`,
click: () => {
const task = TaskFactory.taskForInvertingUnread({
threads: this.threads,
})
Actions.queueTask(task)
},
}
}
starItem() {
const starred = _.every(this.threads, (t) => {
return _.isMatch(t, {starred: false})
});
let dir = ""
let star = "Star"
if (!starred) {
dir = "Remove "
star = (this.threadIds.length > 1) ? "Stars" : "Star"
}
return {
label: `${dir}${star}`,
click: () => {
const task = TaskFactory.taskForInvertingStarred({
threads: this.threads,
})
Actions.queueTask(task)
},
}
}
displayMenu() {
const {remote} = require('electron')
this.menuItemTemplate().then((template) => {
remote.Menu.buildFromTemplate(template)
.popup(remote.getCurrentWindow());
});
}
}

View file

@ -10,7 +10,6 @@ classNames = require 'classnames'
TaskFactory,
ChangeUnreadTask,
WorkspaceStore,
AccountStore,
CategoryStore,
FocusedContentStore,
FocusedPerspectiveStore} = require 'nylas-exports'
@ -20,6 +19,7 @@ ThreadListScrollTooltip = require './thread-list-scroll-tooltip'
ThreadListStore = require './thread-list-store'
FocusContainer = require './focus-container'
EmptyState = require './empty-state'
ThreadListContextMenu = require './thread-list-context-menu'
class ThreadList extends React.Component
@ -36,10 +36,12 @@ class ThreadList extends React.Component
componentDidMount: =>
window.addEventListener('resize', @_onResize, true)
React.findDOMNode(@).addEventListener('contextmenu', @_onShowContextMenu)
@_onResize()
componentWillUnmount: =>
window.removeEventListener('resize', @_onResize, true)
React.findDOMNode(@).removeEventListener('contextmenu', @_onShowContextMenu)
_shift: ({offset, afterRunning}) =>
dataSource = ThreadListStore.dataSource()
@ -99,25 +101,37 @@ class ThreadList extends React.Component
className: classNames
'unread': item.unread
_onDragStart: (event) =>
_threadMouseManipulateData: (event) ->
itemThreadId = @refs.list.itemIdAtPoint(event.clientX, event.clientY)
unless itemThreadId
event.preventDefault()
return
return null
dataSource = ThreadListStore.dataSource()
if itemThreadId in dataSource.selection.ids()
dragThreadIds = dataSource.selection.ids()
dragAccountIds = _.uniq(_.pluck(dataSource.selection.items(), 'accountId'))
threadIds = dataSource.selection.ids()
accountIds = _.uniq(_.pluck(dataSource.selection.items(), 'accountId'))
else
dragThreadIds = [itemThreadId]
dragAccountIds = [dataSource.getById(itemThreadId).accountId]
threadIds = [itemThreadId]
accountIds = [dataSource.getById(itemThreadId).accountId]
dragData = {
accountIds: dragAccountIds,
threadIds: dragThreadIds
return {
accountIds: accountIds,
threadIds: threadIds
}
_onShowContextMenu: (event) =>
data = @_threadMouseManipulateData(event)
if not data
event.preventDefault()
return
(new ThreadListContextMenu(data)).displayMenu()
_onDragStart: (event) =>
dragData = @_threadMouseManipulateData(event)
if not dragData
event.preventDefault()
return
event.dataTransfer.effectAllowed = "move"
event.dataTransfer.dragEffect = "move"

View file

@ -279,10 +279,14 @@ class DraftStore
queries.message = DatabaseStore.find(Message, messageId)
queries.message.include(Message.attributes.body)
else
queries.message = DatabaseStore.findBy(Message, {threadId: threadId ? thread.id}).order(Message.attributes.date.descending()).limit(1)
queries.message.include(Message.attributes.body)
queries.message = @_lastMessageFromThreadId(threadId ? thread.id)
return queries
_lastMessageFromThreadId: (threadId) ->
query = DatabaseStore.findBy(Message, {threadId: threadId ? thread.id}).order(Message.attributes.date.descending()).limit(1)
query.include(Message.attributes.body)
return query
_constructDraft: ({attributes, thread}) =>
account = AccountStore.accountForId(thread.accountId)
throw new Error("Cannot find #{thread.accountId}") unless account