make e-Mail links clickable
[tine20] / tine20 / Felamimail / js / GridDetailsPanel.js
1 /*
2  * Tine 2.0
3  * 
4  * @package     Felamimail
5  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
6  * @author      Philipp Schüle <p.schuele@metaways.de>
7  * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
8  */
9  
10 Ext.ns('Tine.Felamimail');
11
12 /**
13  * @namespace   Tine.Felamimail
14  * @class       Tine.Felamimail.GridDetailsPanel
15  * @extends     Tine.widgets.grid.DetailsPanel
16  * 
17  * <p>Message Grid Details Panel</p>
18  * <p>the details panel (shows message content)</p>
19  * 
20  * TODO         replace telephone numbers in emails with 'call contact' link
21  * TODO         make only text body scrollable (headers should be always visible)
22  * TODO         show image attachments inline
23  * TODO         add 'download all' button
24  * TODO         'from' to contact: check for duplicates
25  * 
26  * @param       {Object} config
27  * @constructor
28  * Create a new Tine.Felamimail.GridDetailsPanel
29  */
30  Tine.Felamimail.GridDetailsPanel = Ext.extend(Tine.widgets.grid.DetailsPanel, {
31     
32     /**
33      * config
34      * @private
35      */
36     defaultHeight: 350,
37     currentId: null,
38     record: null,
39     app: null,
40     i18n: null,
41     
42     fetchBodyTransactionId: null,
43     
44     /**
45      * init
46      * @private
47      */
48     initComponent: function() {
49         this.initTemplate();
50         this.initDefaultTemplate();
51         //this.initTopToolbar();
52         
53         Tine.Felamimail.GridDetailsPanel.superclass.initComponent.call(this);
54     },
55     
56     /**
57      * use default Tpl for default and multi view
58      */
59     initDefaultTemplate: function() {
60         this.defaultTpl = new Ext.XTemplate(
61             '<div class="preview-panel-felamimail">',
62                 '<div class="preview-panel-felamimail-body">{[values ? values.msg : ""]}</div>',
63             '</div>'
64         );
65     },
66     
67     /**
68      * init bottom toolbar (needed for event invitations atm)
69      * 
70      * TODO add buttons (show header, add to addressbook, create filter, show images ...) here
71      */
72 //    initTopToolbar: function() {
73 //        this.tbar = new Ext.Toolbar({
74 //            hidden: true,
75 //            items: []
76 //        });
77 //    },
78     
79     /**
80      * add on click event after render
81      * @private
82      */
83     afterRender: function() {
84         Tine.Felamimail.GridDetailsPanel.superclass.afterRender.apply(this, arguments);
85         this.body.on('click', this.onClick, this);
86     },
87     
88     /**
89      * get panel for single record details
90      * 
91      * @return {Ext.Panel}
92      */
93     getSingleRecordPanel: function() {
94         if (! this.singleRecordPanel) {
95             this.singleRecordPanel = new Ext.Panel({
96                 layout: 'vbox',
97                 layoutConfig: {
98                     align:'stretch'
99                 },
100                 border: false,
101                 items: [
102                     //this.getTopPanel(),
103                     this.getMessageRecordPanel()
104                     //this.getBottomPanel()
105                 ]
106             });
107         }
108         return this.singleRecordPanel;
109     },
110
111     /**
112      * get panel for single record details
113      * 
114      * @return {Ext.Panel}
115      */
116     getMessageRecordPanel: function() {
117         if (! this.messageRecordPanel) {
118             this.messageRecordPanel = new Ext.Panel({
119                 border: false,
120                 autoScroll: true,
121                 flex: 1
122             });
123         }
124         return this.messageRecordPanel;
125     },
126     
127     /**
128      * (on) update details
129      * 
130      * @param {Tine.Felamimail.Model.Message} record
131      * @param {String} body
132      * @private
133      */
134     updateDetails: function(record, body) {
135         if (record.id === this.currentId) {
136             // nothing to do
137         } else if (! record.bodyIsFetched()) {
138             this.waitForContent(record, this.getMessageRecordPanel().body);
139         } else if (record === this.record) {
140             this.setTemplateContent(record, this.getMessageRecordPanel().body);
141         }
142     },
143     
144     /**
145      * wait for body content
146      * 
147      * @param {Tine.Felamimail.Model.Message} record
148      * @param {String} body
149      */
150     waitForContent: function(record, body) {
151         if (! this.grid || this.grid.getSelectionModel().getCount() == 1) {
152             this.refetchBody(record, {
153                 success: this.updateDetails.createDelegate(this, [record, body]),
154                 failure: function (exception) {
155                     Tine.log.debug(exception);
156                     this.getLoadMask().hide();
157                     if (exception.code == 404) {
158                         this.defaultTpl.overwrite(body, {msg: this.app.i18n._('Message not available.')});
159                     } else {
160                         Tine.Felamimail.messageBackend.handleRequestException(exception);
161                     }
162                 },
163                 scope: this
164             });
165             this.defaultTpl.overwrite(body, {msg: ''});
166             this.getLoadMask().show();
167         } else {
168             this.getLoadMask().hide();
169         }
170     },
171     
172     /**
173      * refetch message body
174      * 
175      * @param {Tine.Felamimail.Model.Message} record
176      * @param {Function} callback
177      */
178     refetchBody: function(record, callback) {
179         // cancel old request first
180         if (this.fetchBodyTransactionId && ! Tine.Felamimail.messageBackend.isLoading(this.fetchBodyTransactionId)) {
181             Tine.log.debug('Tine.Felamimail.GridDetailsPanel::refetchBody -> cancelling current fetchBody request.');
182             Tine.Felamimail.messageBackend.abort(this.fetchBodyTransactionId);
183         }
184         Tine.log.debug('Tine.Felamimail.GridDetailsPanel::refetchBody -> calling fetchBody');
185         this.fetchBodyTransactionId = Tine.Felamimail.messageBackend.fetchBody(record, callback);
186     },
187     
188     /**
189      * overwrite template with (body) content
190      * 
191      * @param {Tine.Felamimail.Model.Message} record
192      * @param {String} body
193      * 
194      * TODO allow other prepared parts than email invitations
195      */
196     setTemplateContent: function(record, body) {
197         this.currentId = record.id;
198         this.getLoadMask().hide();
199
200         this.doLayout();
201
202         this.tpl.overwrite(body, record.data);
203         this.getEl().down('div').down('div').scrollTo('top', 0, false);
204         
205         if (this.record.get('preparedParts') && this.record.get('preparedParts').length > 0) {
206             Tine.log.debug('Tine.Felamimail.GridDetailsPanel::setTemplateContent about to handle preparedParts');
207             this.handlePreparedParts(record);
208         }
209     },
210     
211     /**
212      * handle invitation messages (show top + bottom panels)
213      * 
214      * @param {Tine.Felamimail.Model.Message} record
215      */
216     handlePreparedParts: function(record) {
217         var firstPreparedPart = this.record.get('preparedParts')[0],
218             mimeType = String(firstPreparedPart.contentType).split(/[ ;]/)[0],
219             mainType = Tine.Felamimail.MimeDisplayManager.getMainType(mimeType);
220             
221         if (! mainType) {
222             Tine.log.info('Tine.Felamimail.GridDetailsPanel::handlePreparedParts nothing found to handle ' + mimeType);
223             return;
224         }
225         
226         var bodyEl = this.getMessageRecordPanel().getEl().query('div[class=preview-panel-felamimail-body]')[0],
227             detailsPanel = Tine.Felamimail.MimeDisplayManager.create(mainType, {
228                 preparedPart: firstPreparedPart
229             });
230             
231         // quick hack till we have a card body here 
232         Ext.fly(bodyEl).update('');
233         detailsPanel.render(bodyEl);
234     },
235     
236     /**
237      * init single message template (this.tpl)
238      * @private
239      */
240     initTemplate: function() {
241         
242         this.tpl = new Ext.XTemplate(
243             '<div class="preview-panel-felamimail">',
244                 '<div class="preview-panel-felamimail-headers">',
245                     '<b>' + this.i18n._('Subject') + ':</b> {[this.encode(values.subject)]}<br/>',
246                     '<b>' + this.i18n._('From') + ':</b>',
247                     ' {[this.showFrom(values.from_email, values.from_name, "' + this.i18n._('Add') + '", "' 
248                         + this.i18n._('Add contact to addressbook') + '")]}<br/>',
249                     '<b>' + this.i18n._('Date') + ':</b> {[this.showDate(values.sent, values)]}',
250                     '{[this.showRecipients(values.headers)]}',
251                     '{[this.showHeaders("' + this.i18n._('Show or hide header information') + '")]}',
252                 '</div>',
253                 '<div class="preview-panel-felamimail-attachments">{[this.showAttachments(values.attachments, values)]}</div>',
254                 '<div class="preview-panel-felamimail-body">{[this.showBody(values.body, values)]}</div>',
255             '</div>',{
256             app: this.app,
257             panel: this,
258             encode: function(value) {
259                 if (value) {
260                     var encoded = Ext.util.Format.htmlEncode(value);
261                     encoded = Ext.util.Format.nl2br(encoded);
262                     // it should be enough to replace only 2 or more spaces
263                     encoded = encoded.replace(/ /g, '&nbsp;');
264                     
265                     return encoded;
266                 } else {
267                     return '';
268                 }
269                 return value;
270             },
271             
272             showDate: function(sent, messageData) {
273                 var date = (sent) ? sent : messageData.received;
274                 return date.format('l') + ', ' + Tine.Tinebase.common.dateTimeRenderer(date);
275             },
276             
277             showFrom: function(email, name, addText, qtip) {
278                 if (name === null) {
279                     return '';
280                 }
281                 
282                 var result = this.encode(name + ' <' + email + '>');
283                 
284                 // add link with 'add to contacts'
285                 var id = Ext.id() + ':' + email;
286                 
287                 var nameSplit = name.match(/^"*([^,^ ]+)(,*) *(.+)/i);
288                 var firstname = (nameSplit && nameSplit[1]) ? nameSplit[1] : '';
289                 var lastname = (nameSplit && nameSplit[3]) ? nameSplit[3] : '';
290                 if (nameSplit && nameSplit[2] == ',') {
291                     firstname = lastname;
292                     lastname = nameSplit[1];
293                 }
294                 
295                 id += Ext.util.Format.htmlEncode(':' + Ext.util.Format.trim(firstname) + ':' + Ext.util.Format.trim(lastname));
296                 result = '<a id="' + id + '" class="tinebase-email-link">' + result + '</a>'
297                 result += ' <span ext:qtip="' + Tine.Tinebase.common.doubleEncode(qtip) + '" id="' + id + '" class="tinebase-addtocontacts-link">[+]</span>';
298                 return result;
299             },
300             
301             showBody: function(body, messageData) {
302                 body = body || '';
303                 if (body) {
304                     var account = this.app.getActiveAccount();
305                     if (account && (account.get('display_format') == 'plain' || 
306                         (account.get('display_format') == 'content_type' && messageData.body_content_type == 'text/plain'))
307                     ) {
308                         var width = this.panel.body.getWidth()-25,
309                             height = this.panel.body.getHeight()-90,
310                             id = Ext.id();
311                             
312                         if (height < 0) {
313                             // sometimes the height is negative, fix this here
314                             height = 500;
315                         }
316                         
317                         body = '<textarea ' +
318                             'style="width: ' + width + 'px; height: ' + height + 'px; " ' +
319                             'autocomplete="off" id="' + id + '" name="body" class="x-form-textarea x-form-field x-ux-display-background-border" readonly="" >' +
320                             body + '</textarea>';
321                     }
322                 }
323                 return body;
324             },
325             
326             showHeaders: function(qtip) {
327                 var result = ' <span ext:qtip="' + Tine.Tinebase.common.doubleEncode(qtip) + '" id="' + Ext.id() + ':show" class="tinebase-showheaders-link">[...]</span>';
328                 return result;
329             },
330             
331             showRecipients: function(value) {
332                 if (value) {
333                     var i18n = Tine.Tinebase.appMgr.get('Felamimail').i18n,
334                         result = '';
335                     for (header in value) {
336                         if (value.hasOwnProperty(header) && (header == 'to' || header == 'cc' || header == 'bcc')) {
337                             result += '<br/><b>' + i18n._hidden(Ext.util.Format.capitalize(header)) + ':</b> ' 
338                                 + Ext.util.Format.htmlEncode(value[header]);
339                         }
340                     }
341                     return result;
342                 } else {
343                     return '';
344                 }
345             },
346             
347             showAttachments: function(attachments, messageData) {
348                 var result = (attachments.length > 0) ? '<b>' + this.app.i18n._('Attachments') + ':</b> ' : '';
349                 
350                 for (var i=0, id, cls; i < attachments.length; i++) {
351                     result += '<span id="' + Ext.id() + ':' + i + '" class="tinebase-download-link">' 
352                         + '<i>' + attachments[i].filename + '</i>' 
353                         + ' (' + Ext.util.Format.fileSize(attachments[i].size) + ')</span> ';
354                 }
355                 
356                 return result;
357             }
358         });
359     },
360     
361     /**
362      * on click for attachment download / compose dlg / edit contact dlg
363      * 
364      * @param {} e
365      * @private
366      */
367     onClick: function(e) {
368         var selectors = [
369             'span[class=tinebase-download-link]',
370             'a[class=tinebase-email-link]',
371             'span[class=tinebase-addtocontacts-link]',
372             'span[class=tinebase-showheaders-link]'
373         ];
374         
375         // find the correct target
376         for (var i=0, target=null, selector=''; i < selectors.length; i++) {
377             target = e.getTarget(selectors[i]);
378             if (target) {
379                 selector = selectors[i];
380                 break;
381             }
382         }
383         
384         Tine.log.debug('Tine.Felamimail.GridDetailsPanel::onClick found target:"' + selector + '".');
385         
386         switch (selector) {
387             case 'span[class=tinebase-download-link]':
388                 var idx = target.id.split(':')[1],
389                     attachment = this.record.get('attachments')[idx];
390                     
391                 if (! this.record.bodyIsFetched()) {
392                     // sometimes there is bad timing and we do not have the attachments available -> refetch body
393                     this.refetchBody(this.record, this.onClick.createDelegate(this, [e]));
394                     return;
395                 }
396                     
397                 // remove part id if set (that is the case in message/rfc822 attachments)
398                 var messageId = (this.record.id.match(/_/)) ? this.record.id.split('_')[0] : this.record.id;
399                     
400                 if (attachment['content-type'] === 'message/rfc822') {
401                     
402                     Tine.log.debug('Tine.Felamimail.GridDetailsPanel::onClick openWindow for:"' + messageId + '_' + attachment.partId + '".');
403                     // display message
404                     Tine.Felamimail.MessageDisplayDialog.openWindow({
405                         record: new Tine.Felamimail.Model.Message({
406                             id: messageId + '_' + attachment.partId
407                         })
408                     });
409                     
410                 } else {
411                     // download attachment
412                     new Ext.ux.file.Download({
413                         params: {
414                             requestType: 'HTTP',
415                             method: 'Felamimail.downloadAttachment',
416                             messageId: messageId,
417                             partId: attachment.partId
418                         }
419                     }).start();
420                 }
421                 
422                 break;
423                 
424             case 'a[class=tinebase-email-link]':
425                 // open compose dlg
426                 var email = target.id.split(':')[1];
427                 var defaults = Tine.Felamimail.Model.Message.getDefaultData();
428                 defaults.to = [email];
429                 defaults.body = Tine.Felamimail.getSignature();
430                 
431                 var record = new Tine.Felamimail.Model.Message(defaults, 0);
432                 var popupWindow = Tine.Felamimail.MessageEditDialog.openWindow({
433                     record: record
434                 });
435                 break;
436                 
437             case 'span[class=tinebase-addtocontacts-link]':
438                 // open edit contact dlg
439             
440                 // check if addressbook app is available
441                 if (! Tine.Addressbook || ! Tine.Tinebase.common.hasRight('run', 'Addressbook')) {
442                     return;
443                 }
444             
445                 var id = Ext.util.Format.htmlDecode(target.id);
446                 var parts = id.split(':');
447                 
448                 var popupWindow = Tine.Addressbook.ContactEditDialog.openWindow({
449                     listeners: {
450                         scope: this,
451                         'load': function(editdlg) {
452                             editdlg.record.set('email', parts[1]);
453                             editdlg.record.set('n_given', parts[2]);
454                             editdlg.record.set('n_family', parts[3]);
455                         }
456                     }
457                 });
458                 
459                 break;
460                 
461             case 'span[class=tinebase-showheaders-link]':
462                 // show headers
463             
464                 var parts = target.id.split(':');
465                 var targetId = parts[0];
466                 var action = parts[1];
467                 
468                 var html = '';
469                 if (action == 'show') {
470                     var recordHeaders = this.record.get('headers');
471                     
472                     for (header in recordHeaders) {
473                         if (recordHeaders.hasOwnProperty(header) && (header != 'to' || header != 'cc' || header != 'bcc')) {
474                             html += '<br/><b>' + header + ':</b> ' 
475                                 + Ext.util.Format.htmlEncode(recordHeaders[header]);
476                         }
477                     }
478                 
479                     target.id = targetId + ':' + 'hide';
480                     
481                 } else {
482                     html = ' <span ext:qtip="' + Ext.util.Format.htmlEncode(this.i18n._('Show or hide header information')) + '" id="' 
483                         + Ext.id() + ':show" class="tinebase-showheaders-link">[...]</span>'
484                 }
485                 
486                 target.innerHTML = html;
487                 
488                 break;
489         }
490     }
491 });