Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Display the event header #150

Closed
wants to merge 13 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 37 additions & 33 deletions packages/client-app/internal_packages/events/lib/event-header.cjsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@

_ = require 'underscore'
path = require 'path'
React = require 'react'
{RetinaImg} = require 'nylas-component-kit'
icsParser = require './ics-parser'
{Actions,
DateUtils,
Message,
Expand All @@ -19,47 +21,61 @@ class EventHeader extends React.Component
message: React.PropTypes.instanceOf(Message).isRequired

constructor: (@props) ->
if this.props.message.events.length > 12
eventContent = icsParser.convert( @props.message.events );
else
return null;
@state =
event: @props.message.events[0]
event: eventContent

@isEventValid = typeof eventContent == "object";
calendarObj = @state.event.VCALENDAR[0];

@eventDetails = {
"title": calendarObj.VEVENT.SUMMARY,
"start_time": moment(calendarObj.VEVENT.DTSTART + calendarObj.VTIMEZONE.DAYLIGHT.TZOFFSETTO).tz(DateUtils.timeZone),
"end_time": moment(calendarObj.VEVENT.DTEND + calendarObj.VTIMEZONE.DAYLIGHT.TZOFFSETTO).tz(DateUtils.timeZone),
"location": calendarObj.VEVENT.LOCATION,
"description": calendarObj.VEVENT.DESCRIPTION,
"status": calendarObj.VEVENT.STATUS
}

_onChange: =>
return unless @state.event
DatabaseStore.find(Event, @state.event.id).then (event) =>
return unless event
@setState({event})
# TODO
return

componentDidMount: =>
# TODO: This should use observables!
@_unlisten = DatabaseStore.listen (change) =>
if @state.event and change.objectClass is Event.name
updated = _.find change.objects, (o) => o.id is @state.event.id
@setState({event: updated}) if updated
@_onChange()
return

componentWillReceiveProps: (nextProps) =>
@setState({event:nextProps.message.events[0]})
@_onChange()
# TODO
return

componentWillUnmount: =>
@_unlisten?()
#TODO
return

render: =>
timeFormat = DateUtils.getTimeFormat({timeZone: true})
if @state.event?
if @isEventValid?
<div className="event-wrapper">
<div className="event-header">
<RetinaImg name="[email protected]"
mode={RetinaImg.Mode.ContentPreserve}/>
<span className="event-title-text">Event: </span><span className="event-title">{@state.event.title}</span>
<span className="event-title-text">Event: </span><span className="event-title">{@eventDetails.title}</span>
</div>
<div className="event-body">
<div className="event-date">
<div className="event-day">
{moment(@state.event.start*1000).tz(DateUtils.timeZone).format("dddd, MMMM Do")}
{@eventDetails.start_time.format("dddd, MMMM Do")}
</div>
<div>
<div className="event-time">
{moment(@state.event.start*1000).tz(DateUtils.timeZone).format(timeFormat)}
{@eventDetails.start_time.format("h:mm a") + " - " + @eventDetails.end_time.format("h:mm a")}
</div>
<div className="event-description">
{@eventDetails.description}
</div>
{@_renderEventActions()}
</div>
Expand All @@ -70,23 +86,11 @@ class EventHeader extends React.Component
<div></div>

_renderEventActions: =>
me = @state.event.participantForMe()
return false unless me

actions = [["yes", "Accept"], ["maybe", "Maybe"], ["no", "Decline"]]

<div className="event-actions">
{actions.map ([status, label]) =>
classes = "btn-rsvp "
classes += status if me.status is status
<div key={status} className={classes} onClick={=> @_rsvp(status)}>
{label}
</div>
}
</div>
# TODO Later
return

_rsvp: (status) =>
me = @state.event.participantForMe()
Actions.queueTask(new EventRSVPTask(@state.event, me.email, status))
# TODO Later
return

module.exports = EventHeader
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,14 @@ c3 = new ListTabular.Column
flex: 4
resolver: (thread) =>
attachment = false
invite = false

messages = thread.__messages || []

hasInvite = thread.hasAttachments and messages.find (m) -> Utils.showIconForInvites(m.files)
if hasInvite
invite = <div className="thread-icon thread-icon-calendar"></div>

hasAttachments = thread.hasAttachments and messages.find (m) -> Utils.showIconForAttachments(m.files)
if hasAttachments
attachment = <div className="thread-icon thread-icon-attachment"></div>
Expand All @@ -102,6 +108,7 @@ c3 = new ListTabular.Column
<MailLabelSet thread={thread} />
<span className="subject">{subject(thread.subject)}</span>
<span className="snippet">{getSnippet(thread)}</span>
{invite}
{attachment}
</span>

Expand Down Expand Up @@ -142,8 +149,13 @@ cNarrow = new ListTabular.Column
resolver: (thread) =>
pencil = false
attachment = false
invite = false
messages = thread.__messages || []

hasInvite = thread.hasAttachments and messages.find (m) -> Utils.showIconForInvites(m.files)
if hasInvite
invite = <div className="thread-icon thread-icon-calendar"></div>

hasAttachments = thread.hasAttachments and messages.find (m) -> Utils.showIconForAttachments(m.files)
if hasAttachments
attachment = <div className="thread-icon thread-icon-attachment"></div>
Expand Down Expand Up @@ -177,6 +189,7 @@ cNarrow = new ListTabular.Column
<ThreadListParticipants thread={thread} />
{pencil}
<span style={flex:1}></span>
{invite}
{attachment}
<InjectedComponent
key="thread-injected-timestamp"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,11 @@
margin-right:0;
margin-left:0;
}
&.thread-icon-calendar {
background-image:url(../static/images/thread-list/[email protected]);
margin-right:0;
margin-left:0;
}
&.thread-icon-unread {
background-image:url(../static/images/thread-list/[email protected]);
}
Expand Down
8 changes: 2 additions & 6 deletions packages/client-app/src/flux/models/message.es6
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,9 @@ export default class Message extends ModelWithMetadata {
modelKey: 'unread',
}),

events: Attributes.Collection({
events: Attributes.String({
modelKey: 'events',
itemClass: Event,
/* itemClass: Event, */
}),

starred: Attributes.Boolean({
Expand Down Expand Up @@ -229,10 +229,6 @@ Message(date DESC) WHERE draft = 1`,
json.object = 'draft'
}

if (this.events && this.events.length) {
json.event_id = this.events[0].serverId
}

return json
}

Expand Down
4 changes: 4 additions & 0 deletions packages/client-app/src/flux/models/utils.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ Utils =
return false unless files instanceof Array
return files.find (f) -> !f.contentId or f.size > 12 * 1024

showIconForInvites: (files) ->
return false unless files instanceof Array
return files.find (f) -> `f.contentType == "text/calendar"`;

extractTextFromHtml: (html, {maxLength} = {}) ->
if (html ? "").trim().length is 0 then return ""
if maxLength and html.length > maxLength
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ class FetchMessagesInFolderIMAP extends SyncTask {
const desired = [];
const available = [];
const unseen = [struct];
const desiredTypes = new Set(['text/plain', 'text/html']);
const desiredTypes = new Set(['text/plain', 'text/html', 'text/calendar']);
// MIME structures can be REALLY FREAKING COMPLICATED. To simplify
// processing, we flatten the MIME structure by walking it depth-first,
// throwing away all multipart headers with the exception of
Expand All @@ -177,6 +177,12 @@ class FetchMessagesInFolderIMAP extends SyncTask {
part.shift();
const alternativeParts = this._getDesiredMIMEParts(part);
if (alternativeParts.length > 0) {
// We need the ICS body to capture the invite information
alternativeParts.forEach( function(alternativePart) {
if( alternativePart.mimeType == 'text/calendar') {
desired.push(alternativePart);
}
});
// With reference to RFC2046, we keep only the last supported part.
desired.push(alternativeParts[alternativeParts.length - 1]);
}
Expand Down
1 change: 1 addition & 0 deletions packages/client-sync/src/models/message.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ module.exports = (sequelize, Sequelize) => {
}));
},
},
events: Sequelize.TEXT,
subject: Sequelize.STRING(500),
snippet: Sequelize.STRING(255),
date: Sequelize.DATE,
Expand Down
25 changes: 24 additions & 1 deletion packages/isomorphic-core/src/message-utils.es6
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,9 @@ function bodyFromParts(imapMessage, desiredParts) {
//
// This may seem kind of weird, but some MUAs _do_ send out whack stuff
// like an HTML body followed by a plaintext footer.
if (mimeType === 'text/plain') {
if ( mimeType === 'text/calendar' ) {

} else if (mimeType === 'text/plain') {
body += htmlifyPlaintext(decoded);
} else {
body += decoded;
Expand All @@ -184,6 +186,26 @@ function bodyFromParts(imapMessage, desiredParts) {
return body;
}

// Parse events from text/calendar part of the email
function parseEvents(imapMessage, desiredParts) {
let body = '';
for (const {id, mimeType, transferEncoding, charset} of desiredParts) {
let decoded = '';
if ((/quot(ed)?[-/]print(ed|able)?/gi).test(transferEncoding)) {
decoded = mimelib.decodeQuotedPrintable(imapMessage.parts[id], charset);
} else if ((/base64/gi).test(transferEncoding)) {
decoded = mimelib.decodeBase64(imapMessage.parts[id], charset);
} else {
decoded = encoding.convert(imapMessage.parts[id], 'utf-8', charset).toString('utf-8');
}

if ( mimeType === 'text/calendar' && (/base64/gi).test(transferEncoding) ) {
body += decoded;
}
}
return body;
}

// Since we only fetch the MIME structure and specific desired MIME parts from
// IMAP, we unfortunately can't use an existing library like mailparser to parse
// the message, and have to do fun stuff like deal with character sets and
Expand All @@ -210,6 +232,7 @@ async function parseFromImap(imapMessage, desiredParts, {db, accountId, folder})
replyTo: parseContacts(parsedHeaders['reply-to']),
accountId: accountId,
body: bodyFromParts(imapMessage, desiredParts),
events: parseEvents(imapMessage, desiredParts),
snippet: null,
unread: !attributes.flags.includes('\\Seen'),
starred: attributes.flags.includes('\\Flagged'),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* eslint no-unused-vars: 0 */

module.exports = {
up: function up(queryInterface, Sequelize) {
return Sequelize.query("ALTER TABLE messages ADD events TEXT;");
},
down: function down(queryInterface, Sequelize) {
return true;
},
};