mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-01-01 13:14:16 +08:00
feat(rsvp): "Quick RSVP" to events recongized by the API
This commit is contained in:
parent
09728d8cbc
commit
d48fa50654
12 changed files with 112 additions and 279 deletions
|
@ -59,7 +59,7 @@ class PreferencesSignatures extends React.Component
|
||||||
|
|
||||||
_renderAccountPicker: ->
|
_renderAccountPicker: ->
|
||||||
options = @state.accounts.map (account) ->
|
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}>
|
<select value={@state.selectedAccountId} onChange={@_onSelectAccount}>
|
||||||
{options}
|
{options}
|
||||||
|
|
|
@ -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
|
|
93
internal_packages/events/lib/event-header.cjsx
Normal file
93
internal_packages/events/lib/event-header.cjsx
Normal 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
|
|
@ -1,12 +1,12 @@
|
||||||
# {ComponentRegistry, WorkspaceStore} = require 'nylas-exports'
|
{ComponentRegistry, WorkspaceStore} = require 'nylas-exports'
|
||||||
# EventComponent = require "./event-component"
|
EventHeader = require "./event-header"
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
activate: (@state={}) ->
|
activate: (@state={}) ->
|
||||||
# ComponentRegistry.register EventComponent,
|
ComponentRegistry.register EventHeader,
|
||||||
# role: 'Event'
|
role: 'message:BodyHeader'
|
||||||
|
|
||||||
deactivate: ->
|
deactivate: ->
|
||||||
# ComponentRegistry.unregister(EventComponent)
|
ComponentRegistry.unregister(EventHeader)
|
||||||
|
|
||||||
serialize: -> @state
|
serialize: -> @state
|
||||||
|
|
|
@ -6,16 +6,16 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
font-size: @font-size-small;
|
font-size: @font-size-small;
|
||||||
margin-top: @spacing-standard;
|
margin-top: @spacing-standard;
|
||||||
box-shadow: inset 0 0 1px 1px rgba(0,0,0,0.09);
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border-radius: @border-radius-base;
|
border-radius: @border-radius-base;
|
||||||
|
border: 1px solid @border-color-divider;
|
||||||
|
|
||||||
.event-header{
|
.event-header{
|
||||||
border-bottom: 1px solid lighten(@border-color-divider, 6%);
|
border-bottom: 1px solid @border-color-divider;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
||||||
img{
|
img{
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
React = require 'react'
|
React = require 'react'
|
||||||
AutoloadImagesStore = require './autoload-images-store'
|
AutoloadImagesStore = require './autoload-images-store'
|
||||||
Actions = require './autoload-images-actions'
|
Actions = require './autoload-images-actions'
|
||||||
|
{Message} = require 'nylas-exports'
|
||||||
|
|
||||||
class AutoloadImagesHeader extends React.Component
|
class AutoloadImagesHeader extends React.Component
|
||||||
@displayName: 'AutoloadImagesHeader'
|
@displayName: 'AutoloadImagesHeader'
|
||||||
|
|
||||||
|
@propTypes:
|
||||||
|
message: React.PropTypes.instanceOf(Message).isRequired
|
||||||
|
|
||||||
constructor: (@props) ->
|
constructor: (@props) ->
|
||||||
|
|
||||||
render: =>
|
render: =>
|
||||||
|
|
|
@ -80,7 +80,6 @@ class MessageItem extends React.Component
|
||||||
<div className="message-item-area">
|
<div className="message-item-area">
|
||||||
{@_renderHeader()}
|
{@_renderHeader()}
|
||||||
<MessageItemBody message={@props.message} downloads={@state.downloads} />
|
<MessageItemBody message={@props.message} downloads={@state.downloads} />
|
||||||
{@_renderEvents()}
|
|
||||||
{@_renderAttachments()}
|
{@_renderAttachments()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -156,13 +155,6 @@ class MessageItem extends React.Component
|
||||||
else
|
else
|
||||||
<div></div>
|
<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: ->
|
_renderHeaderSideItems: ->
|
||||||
styles =
|
styles =
|
||||||
position: "absolute"
|
position: "absolute"
|
||||||
|
@ -227,16 +219,6 @@ class MessageItem extends React.Component
|
||||||
|
|
||||||
return otherAttachments.concat(imageAttachments)
|
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) ->
|
_isRealFile: (file) ->
|
||||||
hasCIDInBody = file.contentId? and @props.message.body?.indexOf(file.contentId) > 0
|
hasCIDInBody = file.contentId? and @props.message.body?.indexOf(file.contentId) > 0
|
||||||
return not hasCIDInBody
|
return not hasCIDInBody
|
||||||
|
|
|
@ -46,7 +46,9 @@ class PreferencesSidebarItem extends React.Component
|
||||||
"subitem": true
|
"subitem": true
|
||||||
"active": account.id is @props.selection.get('accountId')
|
"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}
|
{account.emailAddress}
|
||||||
</li>
|
</li>
|
||||||
else
|
else
|
||||||
|
|
|
@ -23,6 +23,7 @@ class ConfigSchemaItem extends React.Component
|
||||||
<h2>{_str.humanize(@props.keyName)}</h2>
|
<h2>{_str.humanize(@props.keyName)}</h2>
|
||||||
{_.pairs(@props.configSchema.properties).map ([key, value]) =>
|
{_.pairs(@props.configSchema.properties).map ([key, value]) =>
|
||||||
<ConfigSchemaItem
|
<ConfigSchemaItem
|
||||||
|
key={key}
|
||||||
keyName={key}
|
keyName={key}
|
||||||
keyPath={"#{@props.keyPath}.#{key}"}
|
keyPath={"#{@props.keyPath}.#{key}"}
|
||||||
configSchema={value}
|
configSchema={value}
|
||||||
|
@ -36,7 +37,7 @@ class ConfigSchemaItem extends React.Component
|
||||||
<label htmlFor={@props.keyPath}>{@props.configSchema.title}:</label>
|
<label htmlFor={@props.keyPath}>{@props.configSchema.title}:</label>
|
||||||
<select onChange={@_onChangeValue} value={@props.config.get(@props.keyPath)}>
|
<select onChange={@_onChangeValue} value={@props.config.get(@props.keyPath)}>
|
||||||
{_.zip(@props.configSchema.enum, @props.configSchema.enumLabels).map ([value, label]) =>
|
{_.zip(@props.configSchema.enum, @props.configSchema.enumLabels).map ([value, label]) =>
|
||||||
<option value={value}>{label}</option>
|
<option key={value} value={value}>{label}</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -22,7 +22,7 @@ describe "modelFreeze", ->
|
||||||
b: 2
|
b: 2
|
||||||
Utils.modelFreeze(o)
|
Utils.modelFreeze(o)
|
||||||
expect(Object.isFrozen(o)).toBe(true)
|
expect(Object.isFrozen(o)).toBe(true)
|
||||||
|
|
||||||
it "should not throw an exception when nulls appear in strange places", ->
|
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')
|
t = new Thread(participants: [new Contact(email: 'ben@nylas.com'), null], subject: '123')
|
||||||
Utils.modelFreeze(t)
|
Utils.modelFreeze(t)
|
||||||
|
@ -348,141 +348,6 @@ describe "isEqual", ->
|
||||||
other = {a: 1}
|
other = {a: 1}
|
||||||
ok(!Utils.isEqual(new Foo, other))
|
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&eid=cDA2OGhxNG1yc2xzY…hYzQwYjFhNzEwZWUyZjkxNjI3ZTZlMzk3Yjk4NzQ4YWVhNjA4Nzg&ctz=UTC&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 – 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">•</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">•</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&eid=cDA2OGhxNG1yc2…hYzQwYjFhNzEwZWUyZjkxNjI3ZTZlMzk3Yjk4NzQ4YWVhNjA4Nzg&ctz=UTC&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&eid=cDA2OGhxNG1yc2…hYzQwYjFhNzEwZWUyZjkxNjI3ZTZlMzk3Yjk4NzQ4YWVhNjA4Nzg&ctz=UTC&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&eid=cDA2OGhxNG1yc2…hYzQwYjFhNzEwZWUyZjkxNjI3ZTZlMzk3Yjk4NzQ4YWVhNjA4Nzg&ctz=UTC&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&eid=cDA2OGhxNG1yc2xzY…hYzQwYjFhNzEwZWUyZjkxNjI3ZTZlMzk3Yjk4NzQ4YWVhNjA4Nzg&ctz=UTC&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 <originator@nylas.com><br>
|
|
||||||
Subject: <> Nylas E-mail Call and Webhooks review<br>
|
|
||||||
Date: Jul 28 2015, at 2:32 pm<br>
|
|
||||||
To: random@guy.com <random@guy.com>, Someone Else <someone@else.com>
|
|
||||||
<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", ->
|
describe "subjectWithPrefix", ->
|
||||||
it "should replace an existing Re:", ->
|
it "should replace an existing Re:", ->
|
||||||
expect(Utils.subjectWithPrefix("Re: Test Case", "Fwd:")).toEqual("Fwd: Test Case")
|
expect(Utils.subjectWithPrefix("Re: Test Case", "Fwd:")).toEqual("Fwd: Test Case")
|
||||||
|
|
|
@ -99,13 +99,6 @@ Utils =
|
||||||
return ext in extensions and size > 512 and size < 1024*1024*5
|
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
|
# Escapes potentially dangerous html characters
|
||||||
# This code is lifted from Angular.js
|
# This code is lifted from Angular.js
|
||||||
# See their specs here:
|
# See their specs here:
|
||||||
|
|
|
@ -88,7 +88,7 @@ class NylasExports
|
||||||
# Tasks
|
# Tasks
|
||||||
# These need to be required immediately to populate the TaskRegistry so
|
# These need to be required immediately to populate the TaskRegistry so
|
||||||
# we know how to deserialized saved or IPC-sent tasks.
|
# 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 "SendDraftTask", 'flux/tasks/send-draft'
|
||||||
@require "FileUploadTask", 'flux/tasks/file-upload-task'
|
@require "FileUploadTask", 'flux/tasks/file-upload-task'
|
||||||
@require "DestroyDraftTask", 'flux/tasks/destroy-draft'
|
@require "DestroyDraftTask", 'flux/tasks/destroy-draft'
|
||||||
|
|
Loading…
Reference in a new issue