mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-21 07:46:06 +08:00
RSVP tile for events on messages
Summary: RSVP tile now appears for messages with attached events. Test Plan: Tested manually. Will add unit tests Reviewers: evan, bengotow Reviewed By: bengotow Differential Revision: https://phab.nylas.com/D1797
This commit is contained in:
parent
37c904e875
commit
6609fe272c
|
@ -60,6 +60,7 @@ Exports =
|
|||
DraftCountStore: require '../src/flux/stores/draft-count-store'
|
||||
DraftStoreExtension: require '../src/flux/stores/draft-store-extension'
|
||||
MessageStore: require '../src/flux/stores/message-store'
|
||||
EventStore: require '../src/flux/stores/event-store'
|
||||
MessageStoreExtension: require '../src/flux/stores/message-store-extension'
|
||||
ContactStore: require '../src/flux/stores/contact-store'
|
||||
MetadataStore: require '../src/flux/stores/metadata-store'
|
||||
|
|
102
internal_packages/events/lib/event-component.cjsx
Normal file
102
internal_packages/events/lib/event-component.cjsx
Normal file
|
@ -0,0 +1,102 @@
|
|||
_ = require 'underscore'
|
||||
path = require 'path'
|
||||
React = require 'react'
|
||||
{RetinaImg} = require 'nylas-component-kit'
|
||||
{Actions,
|
||||
Utils,
|
||||
ComponentRegistry,
|
||||
EventStore,
|
||||
NamespaceStore} = require 'nylas-exports'
|
||||
moment = require 'moment-timezone'
|
||||
|
||||
class EventComponent extends React.Component
|
||||
@displayName: 'EventComponent'
|
||||
|
||||
@propTypes:
|
||||
event: React.PropTypes.object.isRequired
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = @_getStateFromStores()
|
||||
|
||||
_onChange: =>
|
||||
@setState(@_getStateFromStores())
|
||||
|
||||
_getStateFromStores: ->
|
||||
e = EventStore.getEvent(@props.event.id)
|
||||
e ?= @props.event
|
||||
|
||||
componentWillMount: ->
|
||||
@unsub = EventStore.listen(@_onChange)
|
||||
|
||||
componentWillUnmount: ->
|
||||
@unsub()
|
||||
|
||||
_myStatus: =>
|
||||
myEmail = NamespaceStore.current()?.me().email
|
||||
for p in @state.participants
|
||||
if p['email'] == myEmail
|
||||
return p['status']
|
||||
|
||||
return null
|
||||
|
||||
render: =>
|
||||
<div className="event-wrapper">
|
||||
<div className="event-header">
|
||||
<RetinaImg name="icon-RSVP-calendar-mini@2x.png"
|
||||
mode={RetinaImg.Mode.ContentPreserve}/>
|
||||
<span className="event-title-text">Event: </span><span className="event-title">{@state.title}</span>
|
||||
</div>
|
||||
<div className="event-body">
|
||||
<div className="event-date">
|
||||
<div className="event-day">
|
||||
{moment(@state.when['start_time']*1000).tz(Utils.timeZone).format("dddd, MMMM Do")}
|
||||
</div>
|
||||
<div>
|
||||
<div className="event-time">
|
||||
{moment(@state.when['start_time']*1000).tz(Utils.timeZone).format("H:mm a z")}
|
||||
</div>
|
||||
{@_renderEventActions()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
_renderEventActions: =>
|
||||
<div className="event-actions">
|
||||
{@_renderAcceptButton()}
|
||||
{@_renderMaybeButton()}
|
||||
{@_renderDeclineButton()}
|
||||
</div>
|
||||
|
||||
_renderAcceptButton: ->
|
||||
classes = "btn-rsvp"
|
||||
if @_myStatus() == "yes"
|
||||
classes += " yes"
|
||||
<div className=classes onClick={@_onClickAccept}>
|
||||
Accept
|
||||
</div>
|
||||
|
||||
_renderDeclineButton: ->
|
||||
classes = "btn-rsvp"
|
||||
if @_myStatus() == "no"
|
||||
classes += " no"
|
||||
<div className=classes onClick={@_onClickDecline}>
|
||||
Decline
|
||||
</div>
|
||||
|
||||
_renderMaybeButton: ->
|
||||
classes = "btn-rsvp"
|
||||
if @_myStatus() == "maybe"
|
||||
classes += " maybe"
|
||||
<div className=classes onClick={@_onClickMaybe}>
|
||||
Maybe
|
||||
</div>
|
||||
|
||||
_onClickAccept: => Actions.RSVPEvent(@state, "yes")
|
||||
|
||||
_onClickDecline: => Actions.RSVPEvent(@state, "no")
|
||||
|
||||
_onClickMaybe: => Actions.RSVPEvent(@state, "maybe")
|
||||
|
||||
|
||||
module.exports = EventComponent
|
13
internal_packages/events/lib/main.cjsx
Normal file
13
internal_packages/events/lib/main.cjsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
{ComponentRegistry, WorkspaceStore} = require 'nylas-exports'
|
||||
|
||||
module.exports =
|
||||
activate: (@state={}) ->
|
||||
EventComponent = require "./event-component"
|
||||
|
||||
ComponentRegistry.register EventComponent,
|
||||
role: 'Event'
|
||||
|
||||
deactivate: ->
|
||||
ComponentRegistry.unregister EventComponent
|
||||
|
||||
serialize: -> @state
|
13
internal_packages/events/package.json
Normal file
13
internal_packages/events/package.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "events",
|
||||
"version": "0.1.0",
|
||||
"main": "./lib/main",
|
||||
"description": "View events on message",
|
||||
"license": "Proprietary",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"atom": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
}
|
||||
}
|
89
internal_packages/events/stylesheets/events.less
Normal file
89
internal_packages/events/stylesheets/events.less
Normal file
|
@ -0,0 +1,89 @@
|
|||
@import "ui-variables";
|
||||
@import "ui-mixins";
|
||||
|
||||
.event-wrapper {
|
||||
cursor: default;
|
||||
position: relative;
|
||||
font-size: @font-size-small;
|
||||
margin-top: @spacing-standard;
|
||||
box-shadow: inset 0 0 1px 1px rgba(0,0,0,0.09);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
border-radius: @border-radius-base;
|
||||
|
||||
.event-header{
|
||||
border-bottom: 1px solid lighten(@border-color-divider, 6%);
|
||||
padding: 10px;
|
||||
|
||||
img{
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.event-title-text{
|
||||
color: @text-color-very-subtle;
|
||||
}
|
||||
|
||||
.event-title{
|
||||
color: @text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.event-body{
|
||||
padding: @padding-small-horizontal;
|
||||
|
||||
.event-date{
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
|
||||
.event-day{
|
||||
display: block;
|
||||
font-size: @font-size-large;
|
||||
color: @text-color-alert;
|
||||
}
|
||||
|
||||
.event-time{
|
||||
display: inline-block;
|
||||
font-size: @font-size-h3;
|
||||
font-weight: @font-weight-blond;
|
||||
}
|
||||
}
|
||||
|
||||
.event-actions {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
|
||||
z-index: 4;
|
||||
text-align: center;
|
||||
|
||||
.btn-rsvp {
|
||||
float: left;
|
||||
padding: @spacing-three-quarters @spacing-sub-double @spacing-three-quarters @spacing-sub-double;
|
||||
line-height: 10px;
|
||||
color: @text-color;
|
||||
border-radius: 3px;
|
||||
background: @background-primary;
|
||||
box-shadow: @standard-shadow;
|
||||
margin: 0 7.5px 0 7.5px;
|
||||
&:active {background: transparent;}
|
||||
|
||||
&.no{
|
||||
background: @error-color;
|
||||
color: @white;
|
||||
}
|
||||
|
||||
&.yes{
|
||||
background: @success-color;
|
||||
color: @white;
|
||||
}
|
||||
|
||||
&.maybe{
|
||||
background: @gray-light;
|
||||
color: @white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ MessageControls = require './message-controls'
|
|||
{Utils,
|
||||
Actions,
|
||||
MessageUtils,
|
||||
NamespaceStore,
|
||||
MessageStore,
|
||||
QuotedHTMLParser,
|
||||
ComponentRegistry,
|
||||
|
@ -79,6 +80,7 @@ class MessageItem extends React.Component
|
|||
{@_formatBody()}
|
||||
</EmailFrame>
|
||||
<a className={@_quotedTextClasses()} onClick={@_toggleQuotedText}></a>
|
||||
{@_renderEvents()}
|
||||
{@_renderAttachments()}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -149,6 +151,13 @@ class MessageItem extends React.Component
|
|||
else
|
||||
<div></div>
|
||||
|
||||
_renderEvents: =>
|
||||
events = @_eventComponents()
|
||||
if events.length > 0 and Utils.looksLikeGmailInvite(@props.message)
|
||||
<div className="events-area">{events}</div>
|
||||
else
|
||||
<div></div>
|
||||
|
||||
_quotedTextClasses: => classNames
|
||||
"quoted-text-control": true
|
||||
'no-quoted-text': not QuotedHTMLParser.hasQuotedHTML(@props.message.body)
|
||||
|
@ -257,6 +266,16 @@ class MessageItem extends React.Component
|
|||
|
||||
return otherAttachments.concat(imageAttachments)
|
||||
|
||||
_eventComponents: =>
|
||||
events = @props.message.events.map (e) =>
|
||||
<InjectedComponent
|
||||
className="event-wrap"
|
||||
matching={role:"Event"}
|
||||
exposedProps={event:e}
|
||||
key={e.id}/>
|
||||
|
||||
return events
|
||||
|
||||
_isRealFile: (file) ->
|
||||
hasCIDInBody = file.contentId? and @props.message.body?.indexOf(file.contentId) > 0
|
||||
return not hasCIDInBody
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
@text-color-inverse: white;
|
||||
@text-color-inverse-subtle: fadeout(@text-color-inverse, 20%);
|
||||
@text-color-inverse-very-subtle: fadeout(@text-color-inverse, 50%);
|
||||
@text-color-heading: #FFF;
|
||||
@text-color-heading: #FFF;
|
||||
|
||||
@border-primary-bg: lighten(@background-primary, 10%);
|
||||
@border-secondary-bg: lighten(@background-secondary, 10%);
|
||||
|
|
88
spec-nylas/models/event-spec.coffee
Normal file
88
spec-nylas/models/event-spec.coffee
Normal file
|
@ -0,0 +1,88 @@
|
|||
Event = require "../../src/flux/models/event"
|
||||
NamespaceStore = require "../../src/flux/stores/namespace-store"
|
||||
|
||||
json_event =
|
||||
{
|
||||
"object": "event",
|
||||
"id": "4ee4xbnx7pxdb9g7c2f8ncyto",
|
||||
"calendar_id": "ci0k1wfyv533ccgox4t7uri4h",
|
||||
"namespace_id": "14e5bn96uizyuhidhcw5rfrb0",
|
||||
"description": null,
|
||||
"location": null,
|
||||
"participants": [
|
||||
{
|
||||
"email": "example@gmail.com",
|
||||
"name": "Ben Bitdiddle",
|
||||
"status": "yes"
|
||||
}
|
||||
],
|
||||
"read_only": false,
|
||||
"title": "Meeting with Ben Bitdiddle",
|
||||
"when": {
|
||||
"object": "timespan",
|
||||
"end_time": 1408123800,
|
||||
"start_time": 1408120200
|
||||
},
|
||||
"busy": true,
|
||||
"status": "confirmed",
|
||||
}
|
||||
|
||||
when_1 =
|
||||
end_time: 1408123800
|
||||
start_time: 1408120200
|
||||
|
||||
participant_1 =
|
||||
name: "Ethan Blackburn"
|
||||
status: "yes"
|
||||
email: "ethan@nylas.com"
|
||||
|
||||
participant_2 =
|
||||
name: "Other Person"
|
||||
status: "maybe"
|
||||
email: "other@person.com"
|
||||
|
||||
participant_3 =
|
||||
name: "Another Person"
|
||||
status: "no"
|
||||
email: "another@person.com"
|
||||
|
||||
event_1 =
|
||||
title: "Dolores"
|
||||
description: "Hanging at the park"
|
||||
location: "Dolores Park"
|
||||
when: when_1
|
||||
start: 1408120200
|
||||
end: 1408123800
|
||||
participants: [participant_1, participant_2, participant_3]
|
||||
|
||||
describe "Event", ->
|
||||
|
||||
it "can be built via the constructor", ->
|
||||
e1 = new Event event_1
|
||||
expect(e1.title).toBe "Dolores"
|
||||
expect(e1.description).toBe "Hanging at the park"
|
||||
expect(e1.location).toBe "Dolores Park"
|
||||
expect(e1.when.start_time).toBe 1408120200
|
||||
expect(e1.when.end_time).toBe 1408123800
|
||||
expect(e1.start).toBe 1408120200
|
||||
expect(e1.end).toBe 1408123800
|
||||
expect(e1.participants[0].name).toBe "Ethan Blackburn"
|
||||
expect(e1.participants[0].email).toBe "ethan@nylas.com"
|
||||
expect(e1.participants[0].status).toBe "yes"
|
||||
expect(e1.participants[1].name).toBe "Other Person"
|
||||
expect(e1.participants[1].email).toBe "other@person.com"
|
||||
expect(e1.participants[1].status).toBe "maybe"
|
||||
expect(e1.participants[2].name).toBe "Another Person"
|
||||
expect(e1.participants[2].email).toBe "another@person.com"
|
||||
expect(e1.participants[2].status).toBe "no"
|
||||
|
||||
it "accepts a JSON response", ->
|
||||
e1 = (new Event).fromJSON(json_event)
|
||||
expect(e1.title).toBe "Meeting with Ben Bitdiddle"
|
||||
expect(e1.description).toBe ''
|
||||
expect(e1.location).toBe ''
|
||||
expect(e1.start).toBe 1408120200
|
||||
expect(e1.end).toBe 1408123800
|
||||
expect(e1.participants[0].name).toBe "Ben Bitdiddle"
|
||||
expect(e1.participants[0].email).toBe "example@gmail.com"
|
||||
expect(e1.participants[0].status).toBe "yes"
|
65
spec-nylas/stores/event-store-spec.coffee
Normal file
65
spec-nylas/stores/event-store-spec.coffee
Normal file
|
@ -0,0 +1,65 @@
|
|||
_ = require 'underscore'
|
||||
proxyquire = require 'proxyquire'
|
||||
Event = require '../../src/flux/models/event'
|
||||
EventStore = require '../../src/flux/stores/event-store'
|
||||
DatabaseStore = require '../../src/flux/stores/database-store'
|
||||
NamespaceStore = require '../../src/flux/stores/namespace-store'
|
||||
|
||||
describe "EventStore", ->
|
||||
beforeEach ->
|
||||
atom.testOrganizationUnit = "folder"
|
||||
EventStore._eventCache = {}
|
||||
EventStore._namespaceId = null
|
||||
EventStore._lastNamespaceId = null
|
||||
NamespaceStore._current =
|
||||
id: "nsid"
|
||||
|
||||
afterEach ->
|
||||
atom.testOrganizationUnit = null
|
||||
|
||||
it "initializes the cache from the DB", ->
|
||||
spyOn(DatabaseStore, "findAll").andCallFake -> Promise.resolve([])
|
||||
EventStore.constructor()
|
||||
advanceClock(30)
|
||||
expect(Object.keys(EventStore._eventCache).length).toBe 0
|
||||
expect(DatabaseStore.findAll).toHaveBeenCalled()
|
||||
|
||||
describe "when the Namespace updates from null to valid", ->
|
||||
beforeEach ->
|
||||
spyOn(EventStore, "_refreshCache")
|
||||
NamespaceStore.trigger()
|
||||
|
||||
it "triggers a database fetch", ->
|
||||
expect(EventStore._refreshCache.calls.length).toBe 1
|
||||
|
||||
describe "when the Namespace updates but the ID doesn't change", ->
|
||||
it "does nothing", ->
|
||||
spyOn(EventStore, "_refreshCache")
|
||||
EventStore._eventCache = {1: '', 2: '', 3: ''}
|
||||
EventStore._namespaceId = "nsid"
|
||||
EventStore._lastNamespaceId = "nsid"
|
||||
NamespaceStore._current =
|
||||
id: "nsid"
|
||||
NamespaceStore.trigger()
|
||||
expect(EventStore._eventCache).toEqual {1: '', 2: '', 3: ''}
|
||||
expect(EventStore._refreshCache).not.toHaveBeenCalled()
|
||||
|
||||
describe "getEvent", ->
|
||||
beforeEach ->
|
||||
@e1 = new Event(id: 1, title:'Test1', start: '', end: '', location: '', participants: [{"name":"Guy", "email":"tester@nylas.com", "status":"noreply"}])
|
||||
@e2 = new Event(id: 2, title:'Test2', start: '', end: '', location: '', participants: [{"name":"Guy", "email":"tester@nylas.com", "status":"noreply"}])
|
||||
@e3 = new Event(id: 3, title:'Test3', start: '', end: '', location: '', participants: [{"name":"Guy", "email":"tester@nylas.com", "status":"noreply"}])
|
||||
@e4 = new Event(id: 4, title:'Test4', start: '', end: '', location: '', participants: [{"name":"Guy", "email":"tester@nylas.com", "status":"noreply"}])
|
||||
EventStore._eventCache = {}
|
||||
for e in [@e1, @e2, @e3, @e4]
|
||||
EventStore._eventCache[e.id] = e
|
||||
|
||||
it "returns event object based on id", ->
|
||||
first = EventStore.getEvent(1)
|
||||
expect(first.title).toBe 'Test1'
|
||||
second = EventStore.getEvent(2)
|
||||
expect(second.title).toBe 'Test2'
|
||||
third = EventStore.getEvent(3)
|
||||
expect(third.title).toBe 'Test3'
|
||||
fourth = EventStore.getEvent(4)
|
||||
expect(fourth.title).toBe 'Test4'
|
91
spec-nylas/tasks/event-rsvp-spec.coffee
Normal file
91
spec-nylas/tasks/event-rsvp-spec.coffee
Normal file
|
@ -0,0 +1,91 @@
|
|||
NylasAPI = require '../../src/flux/nylas-api'
|
||||
Actions = require '../../src/flux/actions'
|
||||
{APIError} = require '../../src/flux/errors'
|
||||
EventRSVPTask = require '../../src/flux/tasks/event-rsvp'
|
||||
DatabaseStore = require '../../src/flux/stores/database-store'
|
||||
Event = require '../../src/flux/models/event'
|
||||
NamespaceStore = require '../../src/flux/stores/namespace-store'
|
||||
_ = require 'underscore'
|
||||
|
||||
describe "EventRSVPTask", ->
|
||||
beforeEach ->
|
||||
spyOn(DatabaseStore, 'find').andCallFake => Promise.resolve(@event)
|
||||
spyOn(DatabaseStore, 'persistModel').andCallFake -> Promise.resolve()
|
||||
@myId = 'nsid'
|
||||
@myName = "Ben Tester"
|
||||
@myEmail = "tester@nylas.com"
|
||||
@event = new Event
|
||||
id: '12233AEDF5'
|
||||
title: 'Meeting with Ben Bitdiddle'
|
||||
description: ''
|
||||
location: ''
|
||||
when:
|
||||
end_time: 1408123800
|
||||
start_time: 1408120200
|
||||
start: 1408120200
|
||||
end: 1408123800
|
||||
participants: [
|
||||
{"name": "Ben Bitdiddle",
|
||||
"email": "ben@bitdiddle.com",
|
||||
"status": "yes"},
|
||||
{"name": @myName,
|
||||
"email": @myEmail,
|
||||
"status": 'noreply'}
|
||||
]
|
||||
@task = new EventRSVPTask(@event, "no")
|
||||
|
||||
describe "performLocal", ->
|
||||
it "should mark our status as no", ->
|
||||
@task.performLocal()
|
||||
advanceClock()
|
||||
expect(@event.participants[1].status).toBe "no"
|
||||
|
||||
it "should trigger an action to persist the change", ->
|
||||
@task.performLocal()
|
||||
advanceClock()
|
||||
expect(DatabaseStore.persistModel).toHaveBeenCalled()
|
||||
|
||||
describe "performRemote", ->
|
||||
it "should make the POST request to the message endpoint", ->
|
||||
spyOn(NylasAPI, 'makeRequest').andCallFake => new Promise (resolve,reject) ->
|
||||
@task.performRemote()
|
||||
options = NylasAPI.makeRequest.mostRecentCall.args[0]
|
||||
expect(options.path).toBe("/n/#{@myId}/send-rsvp")
|
||||
expect(options.method).toBe('POST')
|
||||
expect(options.body.event_id).toBe(@event.id)
|
||||
expect(options.body.status).toBe("no")
|
||||
|
||||
describe "when the remote API request fails", ->
|
||||
beforeEach ->
|
||||
spyOn(NylasAPI, 'makeRequest').andCallFake -> Promise.reject(new APIError(body: '', statusCode: 400))
|
||||
|
||||
it "should not be marked with the status", ->
|
||||
@event = new Event
|
||||
id: '12233AEDF5'
|
||||
title: 'Meeting with Ben Bitdiddle'
|
||||
description: ''
|
||||
location: ''
|
||||
when:
|
||||
end_time: 1408123800
|
||||
start_time: 1408120200
|
||||
start: 1408120200
|
||||
end: 1408123800
|
||||
participants: [
|
||||
{"name": "Ben Bitdiddle",
|
||||
"email": "ben@bitdiddle.com",
|
||||
"status": "yes"},
|
||||
{"name": @myName,
|
||||
"email": @myEmail,
|
||||
"status": 'noreply'}
|
||||
]
|
||||
@task = new EventRSVPTask(@event, "no")
|
||||
@task.performLocal()
|
||||
@task.performRemote()
|
||||
advanceClock()
|
||||
expect(@event.participants[1].status).toBe "noreply"
|
||||
|
||||
it "should trigger an action to persist the change", ->
|
||||
@task.performLocal()
|
||||
@task.performRemote()
|
||||
advanceClock()
|
||||
expect(DatabaseStore.persistModel).toHaveBeenCalled()
|
|
@ -360,6 +360,13 @@ class Actions
|
|||
@searchWeightsChanged: ActionScopeWindow
|
||||
@searchBlurred: ActionScopeWindow
|
||||
|
||||
###
|
||||
Public: Submits the user's response to an RSVP event.
|
||||
|
||||
*Scope: Window*
|
||||
###
|
||||
@RSVPEvent: ActionScopeWindow
|
||||
|
||||
###
|
||||
Public: Fire to display an in-window notification to the user in the app's standard
|
||||
notification interface.
|
||||
|
|
|
@ -1,18 +1,35 @@
|
|||
Model = require './model'
|
||||
Contact = require './contact'
|
||||
Attributes = require '../attributes'
|
||||
_ = require 'underscore'
|
||||
|
||||
class Event extends Model
|
||||
|
||||
@attributes: _.extend {}, Model.attributes,
|
||||
'id': Attributes.String
|
||||
queryable: true
|
||||
modelKey: 'id'
|
||||
jsonKey: 'id'
|
||||
|
||||
'namespaceId': Attributes.String
|
||||
modelKey: 'namespaceId'
|
||||
jsonKey: 'namespaceId'
|
||||
|
||||
'title': Attributes.String
|
||||
modelKey: 'title'
|
||||
jsonKey: 'title'
|
||||
|
||||
'description': Attributes.String
|
||||
modelKey: 'description'
|
||||
jsonKey: 'description'
|
||||
|
||||
'location': Attributes.String
|
||||
modelKey: 'location'
|
||||
jsonKey: 'location'
|
||||
|
||||
'participants': Attributes.Object
|
||||
modelKey: 'participants'
|
||||
jsonKey: 'participants'
|
||||
|
||||
'when': Attributes.Object
|
||||
modelKey: 'when'
|
||||
|
|
|
@ -5,6 +5,7 @@ File = require './file'
|
|||
Label = require './label'
|
||||
Folder = require './folder'
|
||||
Model = require './model'
|
||||
Event = require './event'
|
||||
Contact = require './contact'
|
||||
Attributes = require '../attributes'
|
||||
NamespaceStore = require '../stores/namespace-store'
|
||||
|
@ -106,6 +107,10 @@ class Message extends Model
|
|||
queryable: true
|
||||
modelKey: 'unread'
|
||||
|
||||
'events': Attributes.Collection
|
||||
modelKey: 'events'
|
||||
itemClass: Event
|
||||
|
||||
'starred': Attributes.Boolean
|
||||
queryable: true
|
||||
modelKey: 'starred'
|
||||
|
@ -164,6 +169,7 @@ class Message extends Model
|
|||
@bcc ||= []
|
||||
@replyTo ||= []
|
||||
@files ||= []
|
||||
@events ||= []
|
||||
@
|
||||
|
||||
toJSON: (options) ->
|
||||
|
|
|
@ -65,6 +65,12 @@ Utils =
|
|||
|
||||
return ext in extensions and size > 512 and size < 1024*1024*10
|
||||
|
||||
|
||||
looksLikeGmailInvite: (message={}) ->
|
||||
if 'PRODID:-//Google Inc//Google Calendar' in message.body
|
||||
return true
|
||||
|
||||
|
||||
# Escapes potentially dangerous html characters
|
||||
# This code is lifted from Angular.js
|
||||
# See their specs here:
|
||||
|
@ -114,6 +120,7 @@ Utils =
|
|||
DestroyDraftTask = require '../tasks/destroy-draft'
|
||||
|
||||
FileUploadTask = require '../tasks/file-upload-task'
|
||||
EventRSVP = require '../tasks/event-rsvp'
|
||||
ChangeLabelsTask = require '../tasks/change-labels-task'
|
||||
ChangeFolderTask = require '../tasks/change-folder-task'
|
||||
MarkMessageReadTask = require '../tasks/mark-message-read'
|
||||
|
@ -144,6 +151,7 @@ Utils =
|
|||
'SyncbackDraftTask': SyncbackDraftTask
|
||||
'DestroyDraftTask': DestroyDraftTask
|
||||
'FileUploadTask': FileUploadTask
|
||||
'EventRSVP': EventRSVP
|
||||
}
|
||||
Utils._modelClassMap
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ Reflux = require 'reflux'
|
|||
Actions = require '../actions'
|
||||
Contact = require '../models/contact'
|
||||
Utils = require '../models/utils'
|
||||
NylasStore = require 'nylas-store'
|
||||
RegExpUtils = require '../../regexp-utils'
|
||||
DatabaseStore = require './database-store'
|
||||
NamespaceStore = require './namespace-store'
|
||||
|
@ -30,11 +31,7 @@ _onContactsChanged: ->
|
|||
|
||||
Section: Stores
|
||||
###
|
||||
class ContactStore
|
||||
@include: CoffeeHelpers.includeModule
|
||||
|
||||
@include Publisher
|
||||
@include Listener
|
||||
class ContactStore extends NylasStore
|
||||
|
||||
constructor: ->
|
||||
@_contactCache = []
|
||||
|
|
82
src/flux/stores/event-store.coffee
Normal file
82
src/flux/stores/event-store.coffee
Normal file
|
@ -0,0 +1,82 @@
|
|||
Reflux = require 'reflux'
|
||||
Actions = require '../actions'
|
||||
Event = require '../models/event'
|
||||
Utils = require '../models/utils'
|
||||
NylasStore = require 'nylas-store'
|
||||
DatabaseStore = require './database-store'
|
||||
NamespaceStore = require './namespace-store'
|
||||
_ = require 'underscore'
|
||||
|
||||
EventRSVP = require '../tasks/event-rsvp'
|
||||
|
||||
{Listener, Publisher} = require '../modules/reflux-coffee'
|
||||
CoffeeHelpers = require '../coffee-helpers'
|
||||
|
||||
###
|
||||
Public: EventStore maintains
|
||||
|
||||
## Listening for Changes
|
||||
|
||||
The EventStore monitors the {DatabaseStore} for changes to {Event} models
|
||||
and triggers when events have changed, allowing your stores and components
|
||||
to refresh data based on the EventStore.
|
||||
|
||||
```coffee
|
||||
@unsubscribe = EventStore.listen(@_onEventsChanged, @)
|
||||
|
||||
_onEventsChanged: ->
|
||||
# refresh your event results
|
||||
```
|
||||
|
||||
Section: Stores
|
||||
###
|
||||
class EventStore extends NylasStore
|
||||
|
||||
constructor: ->
|
||||
@_eventCache = {}
|
||||
@_namespaceId = null
|
||||
@listenTo DatabaseStore, @_onDatabaseChanged
|
||||
@listenTo NamespaceStore, @_onNamespaceChanged
|
||||
|
||||
# From Views
|
||||
@listenTo Actions.RSVPEvent, @_onRSVPEvent
|
||||
|
||||
@_refreshCache()
|
||||
|
||||
_onRSVPEvent: (calendar_event, RSVPStatus) ->
|
||||
task = new EventRSVP(calendar_event, RSVPStatus)
|
||||
Actions.queueTask(task)
|
||||
|
||||
__refreshCache: =>
|
||||
new Promise (resolve, reject) =>
|
||||
DatabaseStore.findAll(Event)
|
||||
.then (events=[]) =>
|
||||
@_eventCache[e.id] = e for e in events
|
||||
@trigger()
|
||||
resolve()
|
||||
.catch (err) ->
|
||||
console.warn("Request for Events failed. #{err}")
|
||||
_refreshCache: _.debounce(EventStore::__refreshCache, 20)
|
||||
|
||||
_onDatabaseChanged: (change) =>
|
||||
return unless change?.objectClass is Event.name
|
||||
for e in change.objects
|
||||
@_eventCache[e.id] = e
|
||||
|
||||
_resetCache: =>
|
||||
@_eventCache = {}
|
||||
@trigger(@)
|
||||
|
||||
getEvent: (id) =>
|
||||
@_eventCache[id]
|
||||
|
||||
_onNamespaceChanged: =>
|
||||
return if @_namespaceId is NamespaceStore.current()?.id
|
||||
@_namespaceId = NamespaceStore.current()?.id
|
||||
|
||||
if @_namespaceId
|
||||
@_refreshCache()
|
||||
else
|
||||
@_resetCache()
|
||||
|
||||
module.exports = new EventStore()
|
49
src/flux/tasks/event-rsvp.coffee
Normal file
49
src/flux/tasks/event-rsvp.coffee
Normal file
|
@ -0,0 +1,49 @@
|
|||
Task = require './task'
|
||||
Event = require '../models/event'
|
||||
{APIError} = require '../errors'
|
||||
Utils = require '../models/utils'
|
||||
DatabaseStore = require '../stores/database-store'
|
||||
NamespaceStore = require '../stores/namespace-store'
|
||||
Actions = require '../actions'
|
||||
NylasAPI = require '../nylas-api'
|
||||
|
||||
module.exports =
|
||||
class EventRSVPTask extends Task
|
||||
constructor: (calendar_event, @RSVPResponse) ->
|
||||
@myEmail = NamespaceStore.current()?.me().email.toLowerCase().trim()
|
||||
@event = calendar_event
|
||||
super
|
||||
|
||||
performLocal: ->
|
||||
DatabaseStore.find(Event, @event.id).then (e) =>
|
||||
e ?= @event
|
||||
@_previousParticipantsState = Utils.deepClone(e.participants)
|
||||
participants = []
|
||||
for p in e.participants
|
||||
if p['email'] == @myEmail
|
||||
p['status'] = @RSVPResponse
|
||||
participants.push p
|
||||
e.participants = participants
|
||||
@event = e
|
||||
DatabaseStore.persistModel(e)
|
||||
|
||||
performRemote: ->
|
||||
new Promise (resolve, reject) =>
|
||||
NylasAPI.makeRequest
|
||||
path: "/n/#{NamespaceStore.current()?.id}/send-rsvp"
|
||||
method: "POST"
|
||||
body: {
|
||||
event_id: @event.id,
|
||||
status: @RSVPResponse
|
||||
}
|
||||
returnsModel: true
|
||||
.then =>
|
||||
return Promise.resolve(Task.Status.Finished)
|
||||
.catch APIError, (err) =>
|
||||
##TODO: event already accepted/declined/etc
|
||||
@event.participants = @_previousParticipantsState
|
||||
DatabaseStore.persistModel(@event).then(resolve).catch(reject)
|
||||
|
||||
onOtherError: -> Promise.resolve()
|
||||
onTimeoutError: -> Promise.resolve()
|
||||
onOfflineError: -> Promise.resolve()
|
BIN
static/images/events/icon-RSVP-calendar-mini@2x.png
Normal file
BIN
static/images/events/icon-RSVP-calendar-mini@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 633 B |
|
@ -69,6 +69,7 @@
|
|||
@text-color-inverse: @white;
|
||||
@text-color-inverse-subtle: fadeout(@text-color-inverse, 20%);
|
||||
@text-color-inverse-very-subtle: fadeout(@text-color-inverse, 50%);
|
||||
@text-color-alert: #e64d65;
|
||||
|
||||
@text-color-heading: @nylas-gray;
|
||||
|
||||
|
|
Loading…
Reference in a new issue