feat(rsvp): "Quick RSVP" to events recongized by the API

This commit is contained in:
Ben Gotow 2015-11-30 17:11:57 -08:00
parent 09728d8cbc
commit d48fa50654
12 changed files with 112 additions and 279 deletions

View file

@ -59,7 +59,7 @@ class PreferencesSignatures extends React.Component
_renderAccountPicker: ->
options = @state.accounts.map (account) ->
<option value={account.id}>{account.emailAddress}</option>
<option value={account.id} key={account.id}>{account.emailAddress}</option>
<select value={@state.selectedAccountId} onChange={@_onSelectAccount}>
{options}

View file

@ -1,107 +0,0 @@
_ = require 'underscore'
path = require 'path'
React = require 'react'
{RetinaImg} = require 'nylas-component-kit'
{Actions,
Event,
Utils,
ComponentRegistry,
AccountStore} = require 'nylas-exports'
EventRSVPTask = require './tasks/event-rsvp'
moment = require 'moment-timezone'
class EventComponent extends React.Component
@displayName: 'EventComponent'
@propTypes:
event: React.PropTypes.object.isRequired
constructor: (@props) ->
# Since getting state is asynchronous, default to empty values
@state = @_nullEvent()
_nullEvent: ->
participants: []
title: ""
when: {start_time: 0}
_onChange: =>
DatabaseStore.find(Event, @props.event.id).then (event) =>
event ?= @_nullEvent()
@setState(event)
componentDidMount: -> @_onChange()
componentWillMount: ->
@usubs.push DatabaseStore.listen (change) =>
@_onChange() if change.objectClass is Event.name
@usubs.push AccountStore.listen(@_onChange)
componentWillUnmount: -> usub?() for usub in @usubs()
_myStatus: =>
myEmail = AccountStore.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={=> @_rsvp("yes")}>
Accept
</div>
_renderDeclineButton: ->
classes = "btn-rsvp"
if @_myStatus() == "no"
classes += " no"
<div className=classes onClick={=> @_rsvp("no")}>
Decline
</div>
_renderMaybeButton: ->
classes = "btn-rsvp"
if @_myStatus() == "maybe"
classes += " maybe"
<div className=classes onClick={=> @_rsvp("maybe")}>
Maybe
</div>
_rsvp: (status) ->
Acitions.queueTask(new EventRSVPTask(@state, status))
module.exports = EventComponent

View file

@ -0,0 +1,93 @@
_ = require 'underscore'
path = require 'path'
React = require 'react'
{RetinaImg} = require 'nylas-component-kit'
{Actions,
Message,
Event,
Utils,
ComponentRegistry,
EventRSVPTask,
DatabaseStore,
AccountStore} = require 'nylas-exports'
moment = require 'moment-timezone'
class EventHeader extends React.Component
@displayName: 'EventHeader'
@propTypes:
message: React.PropTypes.instanceOf(Message).isRequired
constructor: (@props) ->
@state =
event: @props.message.events[0]
_onChange: =>
return unless @state.event
DatabaseStore.find(Event, @state.event.id).then (event) =>
return unless event
@setState({event})
componentDidMount: =>
@_unlisten = DatabaseStore.listen (change) =>
if change.objectClass is Event.name
updated = _.find change.objects, (o) => o.id is @state.event.id
@setState({event: updated}) if updated
@_onChange()
componentWillReceiveProps: (nextProps) =>
@setState({event:nextProps.message.events[0]})
@_onChange()
componentWillUnmount: =>
@_unlisten?()
_myStatus: =>
myEmail = AccountStore.current()?.me().email
for p in @state.event.participants
return p['status'] if p['email'] is myEmail
return null
render: =>
if @state.event?
<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.event.title}</span>
</div>
<div className="event-body">
<div className="event-date">
<div className="event-day">
{moment(@state.event.when['start_time']*1000).tz(Utils.timeZone).format("dddd, MMMM Do")}
</div>
<div>
<div className="event-time">
{moment(@state.event.when['start_time']*1000).tz(Utils.timeZone).format("h:mm a z")}
</div>
{@_renderEventActions()}
</div>
</div>
</div>
</div>
else
<div></div>
_renderEventActions: =>
actions = [["yes", "Accept"], ["maybe", "Maybe"], ["no", "Decline"]]
<div className="event-actions">
{actions.map ([status, label]) =>
classes = "btn-rsvp "
classes += status if @_myStatus() is status
<div key={status} className={classes} onClick={=> @_rsvp(status)}>
{label}
</div>
}
</div>
_rsvp: (status) =>
Actions.queueTask(new EventRSVPTask(@state.event, status))
module.exports = EventHeader

View file

@ -1,12 +1,12 @@
# {ComponentRegistry, WorkspaceStore} = require 'nylas-exports'
# EventComponent = require "./event-component"
{ComponentRegistry, WorkspaceStore} = require 'nylas-exports'
EventHeader = require "./event-header"
module.exports =
activate: (@state={}) ->
# ComponentRegistry.register EventComponent,
# role: 'Event'
ComponentRegistry.register EventHeader,
role: 'message:BodyHeader'
deactivate: ->
# ComponentRegistry.unregister(EventComponent)
ComponentRegistry.unregister(EventHeader)
serialize: -> @state

View file

@ -6,16 +6,16 @@
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;
border: 1px solid @border-color-divider;
.event-header{
border-bottom: 1px solid lighten(@border-color-divider, 6%);
border-bottom: 1px solid @border-color-divider;
padding: 10px;
img{

View file

@ -1,10 +1,14 @@
React = require 'react'
AutoloadImagesStore = require './autoload-images-store'
Actions = require './autoload-images-actions'
{Message} = require 'nylas-exports'
class AutoloadImagesHeader extends React.Component
@displayName: 'AutoloadImagesHeader'
@propTypes:
message: React.PropTypes.instanceOf(Message).isRequired
constructor: (@props) ->
render: =>

View file

@ -80,7 +80,6 @@ class MessageItem extends React.Component
<div className="message-item-area">
{@_renderHeader()}
<MessageItemBody message={@props.message} downloads={@state.downloads} />
{@_renderEvents()}
{@_renderAttachments()}
</div>
</div>
@ -156,13 +155,6 @@ class MessageItem extends React.Component
else
<div></div>
_renderEvents: =>
events = @_eventComponents()
if events.length > 0 and not Utils.looksLikeGmailInvite(@props.message)
<div className="events-area">{events}</div>
else
<div></div>
_renderHeaderSideItems: ->
styles =
position: "absolute"
@ -227,16 +219,6 @@ 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

View file

@ -46,7 +46,9 @@ class PreferencesSidebarItem extends React.Component
"subitem": true
"active": account.id is @props.selection.get('accountId')
<li className={classes} onClick={ (event) => @_onClickAccount(event, account.id)}>
<li key={account.id}
className={classes}
onClick={ (event) => @_onClickAccount(event, account.id)}>
{account.emailAddress}
</li>
else

View file

@ -23,6 +23,7 @@ class ConfigSchemaItem extends React.Component
<h2>{_str.humanize(@props.keyName)}</h2>
{_.pairs(@props.configSchema.properties).map ([key, value]) =>
<ConfigSchemaItem
key={key}
keyName={key}
keyPath={"#{@props.keyPath}.#{key}"}
configSchema={value}
@ -36,7 +37,7 @@ class ConfigSchemaItem extends React.Component
<label htmlFor={@props.keyPath}>{@props.configSchema.title}:</label>
<select onChange={@_onChangeValue} value={@props.config.get(@props.keyPath)}>
{_.zip(@props.configSchema.enum, @props.configSchema.enumLabels).map ([value, label]) =>
<option value={value}>{label}</option>
<option key={value} value={value}>{label}</option>
}
</select>
</div>

View file

@ -22,7 +22,7 @@ describe "modelFreeze", ->
b: 2
Utils.modelFreeze(o)
expect(Object.isFrozen(o)).toBe(true)
it "should not throw an exception when nulls appear in strange places", ->
t = new Thread(participants: [new Contact(email: 'ben@nylas.com'), null], subject: '123')
Utils.modelFreeze(t)
@ -348,141 +348,6 @@ describe "isEqual", ->
other = {a: 1}
ok(!Utils.isEqual(new Foo, other))
describe "looksLikeGmailInvite", ->
it "should return false for an exchange invite", ->
message = {
body: """<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
</head>
<body>
<div style="">
<table cellspacing="0" cellpadding="8" border="0" summary="" style="width:100%;font-family:Arial,Sans-serif;border:1px Solid #ccc;border-width:1px 2px 2px 1px;background-color:#fff;" itemscope="" itemtype="http://schema.org/Event">
<tbody>
<tr>
<td>
<meta itemprop="eventStatus" content="http://schema.org/EventScheduled">
<div style="padding:2px"><span itemprop="publisher" itemscope="" itemtype="http://schema.org/Organization">
<meta itemprop="name" content="Google Calendar">
</span>
<meta itemprop="eventId/googleCalendar" content="p068hq4mrslsbnddg0a8kdfg2s">
<div style="float:right;font-weight:bold;font-size:13px"><a href="https://www.google.com/calendar/event?action=VIEW&amp;eid=cDA2OGhxNG1yc2xzY…hYzQwYjFhNzEwZWUyZjkxNjI3ZTZlMzk3Yjk4NzQ4YWVhNjA4Nzg&amp;ctz=UTC&amp;hl=en" style="color:#20c;white-space:nowrap" itemprop="url">more
details »</a><br>
</div>
<h3 style="padding:0 0 6px 0;margin:0;font-family:Arial,Sans-serif;font-size:16px;font-weight:bold;color:#222">
<span itemprop="name">(No Subject)</span></h3>
<table cellpadding="0" cellspacing="0" border="0" summary="Event details">
<tbody>
<tr>
<td style="padding:0 1em 10px 0;font-family:Arial,Sans-serif;font-size:13px;color:#888;white-space:nowrap" valign="top">
<div><i style="font-style:normal">When</i></div>
</td>
<td style="padding-bottom:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222" valign="top">
<time itemprop="startDate" datetime="20150730T220000Z"></time><time itemprop="endDate" datetime="20150730T230000Z"></time>Thu Jul 30, 2015 10pm &#8211; 11pm
<span style="color:#888">GMT (no daylight saving)</span></td>
</tr>
<tr>
<td style="padding:0 1em 10px 0;font-family:Arial,Sans-serif;font-size:13px;color:#888;white-space:nowrap" valign="top">
<div><i style="font-style:normal">Calendar</i></div>
</td>
<td style="padding-bottom:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222" valign="top">
ethan@nylas.com</td>
</tr>
<tr>
<td style="padding:0 1em 10px 0;font-family:Arial,Sans-serif;font-size:13px;color:#888;white-space:nowrap" valign="top">
<div><i style="font-style:normal">Who</i></div>
</td>
<td style="padding-bottom:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222" valign="top">
<table cellspacing="0" cellpadding="0">
<tbody>
<tr>
<td style="padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222">
<span style="font-family:Courier New,monospace">&#8226;</span></td>
<td style="padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222">
<div>
<div style="margin:0 0 0.3em 0"><span itemprop="attendee" itemscope="" itemtype="http://schema.org/Person"><span itemprop="name">Ethan Blackburn</span>
<meta itemprop="email" content="eblackb1@slu.edu">
</span><span style="font-size:11px;color:#888"> - organizer</span></div>
</div>
</td>
</tr>
<tr>
<td style="padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222">
<span style="font-family:Courier New,monospace">&#8226;</span></td>
<td style="padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222">
<div>
<div style="margin:0 0 0.3em 0"><span itemprop="attendee" itemscope="" itemtype="http://schema.org/Person"><span itemprop="name">ethan@nylas.com</span>
<meta itemprop="email" content="ethan@nylas.com">
</span></div>
</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<p style="color:#222;font-size:13px;margin:0"><span style="color:#888">Going? </span>
<wbr><strong><span itemprop="action" itemscope="" itemtype="http://schema.org/RsvpAction">
<meta itemprop="attendance" content="http://schema.org/RsvpAttendance/Yes">
<span itemprop="handler" itemscope="" itemtype="http://schema.org/HttpActionHandler"><link itemprop="method" href="http://schema.org/HttpRequestMethod/GET"><a href="https://www.google.com/calendar/event?action=RESPOND&amp;eid=cDA2OGhxNG1yc2…hYzQwYjFhNzEwZWUyZjkxNjI3ZTZlMzk3Yjk4NzQ4YWVhNjA4Nzg&amp;ctz=UTC&amp;hl=en" style="color:#20c;white-space:nowrap" itemprop="url">Yes</a></span></span><span style="margin:0 0.4em;font-weight:normal">
- </span><span itemprop="action" itemscope="" itemtype="http://schema.org/RsvpAction">
<meta itemprop="attendance" content="http://schema.org/RsvpAttendance/Maybe">
<span itemprop="handler" itemscope="" itemtype="http://schema.org/HttpActionHandler"><link itemprop="method" href="http://schema.org/HttpRequestMethod/GET"><a href="https://www.google.com/calendar/event?action=RESPOND&amp;eid=cDA2OGhxNG1yc2…hYzQwYjFhNzEwZWUyZjkxNjI3ZTZlMzk3Yjk4NzQ4YWVhNjA4Nzg&amp;ctz=UTC&amp;hl=en" style="color:#20c;white-space:nowrap" itemprop="url">Maybe</a></span></span><span style="margin:0 0.4em;font-weight:normal">
- </span><span itemprop="action" itemscope="" itemtype="http://schema.org/RsvpAction">
<meta itemprop="attendance" content="http://schema.org/RsvpAttendance/No">
<span itemprop="handler" itemscope="" itemtype="http://schema.org/HttpActionHandler"><link itemprop="method" href="http://schema.org/HttpRequestMethod/GET"><a href="https://www.google.com/calendar/event?action=RESPOND&amp;eid=cDA2OGhxNG1yc2…hYzQwYjFhNzEwZWUyZjkxNjI3ZTZlMzk3Yjk4NzQ4YWVhNjA4Nzg&amp;ctz=UTC&amp;hl=en" style="color:#20c;white-space:nowrap" itemprop="url">No</a></span></span></strong>
<wbr><a href="https://www.google.com/calendar/event?action=VIEW&amp;eid=cDA2OGhxNG1yc2xzY…hYzQwYjFhNzEwZWUyZjkxNjI3ZTZlMzk3Yjk4NzQ4YWVhNjA4Nzg&amp;ctz=UTC&amp;hl=en" style="color:#20c;white-space:nowrap" itemprop="url">more
options »</a></p>
</td>
</tr>
<tr>
<td style="background-color:#f6f6f6;color:#888;border-top:1px Solid #ccc;font-family:Arial,Sans-serif;font-size:11px">
<p>Invitation from <a href="https://www.google.com/calendar/" target="_blank" style="">
Google Calendar</a></p>
<p>You are receiving this email at the account ethan@nylas.com because you are subscribed for invitations on calendar ethan@nylas.com.</p>
<p>To stop receiving these emails, please log in to https://www.google.com/calendar/ and change your notification settings for this calendar.</p>
<p>Forwarding this invitation could allow any recipient to modify your RSVP response.
<a href="https://support.google.com/calendar/answer/37135#forwarding">Learn More</a>.</p>
</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>"""
}
expect(Utils.looksLikeGmailInvite(message)).toEqual(true)
it "should return true for a gmail invite", ->
message = {
body: """<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=us-ascii">
</head>
<body>
<br>
<br>
<blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;">
Begin forwarded message: <br>
<br>
From: Originator &lt;originator@nylas.com&gt;<br>
Subject: &lt;&gt; Nylas E-mail Call and Webhooks review<br>
Date: Jul 28 2015, at 2:32 pm<br>
To: random@guy.com &lt;random@guy.com&gt;, Someone Else &lt;someone@else.com&gt;
<br>
<br>
<style type="text/css" style="display:none;"><!-- P {margin-top:0;margin-bottom:0;} --></style>
<div id="divtagdefaultwrapper" style="font-size:12pt;color:#000000;background-color:#FFFFFF;font-family:Calibri,Arial,Helvetica,sans-serif;">
Call to review Pipeline Deals email tool and webhooks support.</div>
</blockquote>
</body>
</html>"""
}
describe "subjectWithPrefix", ->
it "should replace an existing Re:", ->
expect(Utils.subjectWithPrefix("Re: Test Case", "Fwd:")).toEqual("Fwd: Test Case")

View file

@ -99,13 +99,6 @@ Utils =
return ext in extensions and size > 512 and size < 1024*1024*5
looksLikeGmailInvite: (message={}) ->
idx = message.body.search('itemtype="http://schema.org/Event"')
if idx == -1
return false
return true
# Escapes potentially dangerous html characters
# This code is lifted from Angular.js
# See their specs here:

View file

@ -88,7 +88,7 @@ class NylasExports
# Tasks
# These need to be required immediately to populate the TaskRegistry so
# we know how to deserialized saved or IPC-sent tasks.
@require "EventRSVP", 'flux/tasks/event-rsvp'
@require "EventRSVPTask", 'flux/tasks/event-rsvp'
@require "SendDraftTask", 'flux/tasks/send-draft'
@require "FileUploadTask", 'flux/tasks/file-upload-task'
@require "DestroyDraftTask", 'flux/tasks/destroy-draft'