5ae2bbebd0fd032ead168548c92db54c401dc585
[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 += ' <span ext:qtip="' + Tine.Tinebase.common.doubleEncode(qtip) + '" id="' + id + '" class="tinebase-addtocontacts-link">[+]</span>';
297                 return result;
298             },
299             
300             showBody: function(body, messageData) {
301                 body = body || '';
302                 if (body) {
303                     var account = this.app.getActiveAccount();
304                     if (account && (account.get('display_format') == 'plain' || 
305                         (account.get('display_format') == 'content_type' && messageData.body_content_type == 'text/plain'))
306                     ) {
307                         var width = this.panel.body.getWidth()-25,
308                             height = this.panel.body.getHeight()-90,
309                             id = Ext.id();
310                             
311                         if (height < 0) {
312                             // sometimes the height is negative, fix this here
313                             height = 500;
314                         }
315                         
316                         body = '<textarea ' +
317                             'style="width: ' + width + 'px; height: ' + height + 'px; " ' +
318                             'autocomplete="off" id="' + id + '" name="body" class="x-form-textarea x-form-field x-ux-display-background-border" readonly="" >' +
319                             body + '</textarea>';
320                     }
321                 }
322                 return body;
323             },
324             
325             showHeaders: function(qtip) {
326                 var result = ' <span ext:qtip="' + Tine.Tinebase.common.doubleEncode(qtip) + '" id="' + Ext.id() + ':show" class="tinebase-showheaders-link">[...]</span>';
327                 return result;
328             },
329             
330             showRecipients: function(value) {
331                 if (value) {
332                     var i18n = Tine.Tinebase.appMgr.get('Felamimail').i18n,
333                         result = '';
334                     for (header in value) {
335                         if (value.hasOwnProperty(header) && (header == 'to' || header == 'cc' || header == 'bcc')) {
336                             result += '<br/><b>' + i18n._hidden(Ext.util.Format.capitalize(header)) + ':</b> ' 
337                                 + Ext.util.Format.htmlEncode(value[header]);
338                         }
339                     }
340                     return result;
341                 } else {
342                     return '';
343                 }
344             },
345             
346             showAttachments: function(attachments, messageData) {
347                 var result = (attachments.length > 0) ? '<b>' + this.app.i18n._('Attachments') + ':</b> ' : '';
348                 
349                 for (var i=0, id, cls; i < attachments.length; i++) {
350                     result += '<span id="' + Ext.id() + ':' + i + '" class="tinebase-download-link">' 
351                         + '<i>' + attachments[i].filename + '</i>' 
352                         + ' (' + Ext.util.Format.fileSize(attachments[i].size) + ')</span> ';
353                 }
354                 
355                 return result;
356             }
357         });
358     },
359     
360     /**
361      * on click for attachment download / compose dlg / edit contact dlg
362      * 
363      * @param {} e
364      * @private
365      */
366     onClick: function(e) {
367         var selectors = [
368             'span[class=tinebase-download-link]',
369             'a[class=tinebase-email-link]',
370             'span[class=tinebase-addtocontacts-link]',
371             'span[class=tinebase-showheaders-link]'
372         ];
373         
374         // find the correct target
375         for (var i=0, target=null, selector=''; i < selectors.length; i++) {
376             target = e.getTarget(selectors[i]);
377             if (target) {
378                 selector = selectors[i];
379                 break;
380             }
381         }
382         
383         Tine.log.debug('Tine.Felamimail.GridDetailsPanel::onClick found target:"' + selector + '".');
384         
385         switch (selector) {
386             case 'span[class=tinebase-download-link]':
387                 var idx = target.id.split(':')[1],
388                     attachment = this.record.get('attachments')[idx];
389                     
390                 if (! this.record.bodyIsFetched()) {
391                     // sometimes there is bad timing and we do not have the attachments available -> refetch body
392                     this.refetchBody(this.record, this.onClick.createDelegate(this, [e]));
393                     return;
394                 }
395                     
396                 // remove part id if set (that is the case in message/rfc822 attachments)
397                 var messageId = (this.record.id.match(/_/)) ? this.record.id.split('_')[0] : this.record.id;
398                     
399                 if (attachment['content-type'] === 'message/rfc822') {
400                     
401                     Tine.log.debug('Tine.Felamimail.GridDetailsPanel::onClick openWindow for:"' + messageId + '_' + attachment.partId + '".');
402                     // display message
403                     Tine.Felamimail.MessageDisplayDialog.openWindow({
404                         record: new Tine.Felamimail.Model.Message({
405                             id: messageId + '_' + attachment.partId
406                         })
407                     });
408                     
409                 } else {
410                     // download attachment
411                     new Ext.ux.file.Download({
412                         params: {
413                             requestType: 'HTTP',
414                             method: 'Felamimail.downloadAttachment',
415                             messageId: messageId,
416                             partId: attachment.partId
417                         }
418                     }).start();
419                 }
420                 
421                 break;
422                 
423             case 'a[class=tinebase-email-link]':
424                 // open compose dlg
425                 var email = target.id.split(':')[1];
426                 var defaults = Tine.Felamimail.Model.Message.getDefaultData();
427                 defaults.to = [email];
428                 defaults.body = Tine.Felamimail.getSignature();
429                 
430                 var record = new Tine.Felamimail.Model.Message(defaults, 0);
431                 var popupWindow = Tine.Felamimail.MessageEditDialog.openWindow({
432                     record: record
433                 });
434                 break;
435                 
436             case 'span[class=tinebase-addtocontacts-link]':
437                 // open edit contact dlg
438             
439                 // check if addressbook app is available
440                 if (! Tine.Addressbook || ! Tine.Tinebase.common.hasRight('run', 'Addressbook')) {
441                     return;
442                 }
443             
444                 var id = Ext.util.Format.htmlDecode(target.id);
445                 var parts = id.split(':');
446                 
447                 var popupWindow = Tine.Addressbook.ContactEditDialog.openWindow({
448                     listeners: {
449                         scope: this,
450                         'load': function(editdlg) {
451                             editdlg.record.set('email', parts[1]);
452                             editdlg.record.set('n_given', parts[2]);
453                             editdlg.record.set('n_family', parts[3]);
454                         }
455                     }
456                 });
457                 
458                 break;
459                 
460             case 'span[class=tinebase-showheaders-link]':
461                 // show headers
462             
463                 var parts = target.id.split(':');
464                 var targetId = parts[0];
465                 var action = parts[1];
466                 
467                 var html = '';
468                 if (action == 'show') {
469                     var recordHeaders = this.record.get('headers');
470                     
471                     for (header in recordHeaders) {
472                         if (recordHeaders.hasOwnProperty(header) && (header != 'to' || header != 'cc' || header != 'bcc')) {
473                             html += '<br/><b>' + header + ':</b> ' 
474                                 + Ext.util.Format.htmlEncode(recordHeaders[header]);
475                         }
476                     }
477                 
478                     target.id = targetId + ':' + 'hide';
479                     
480                 } else {
481                     html = ' <span ext:qtip="' + Ext.util.Format.htmlEncode(this.i18n._('Show or hide header information')) + '" id="' 
482                         + Ext.id() + ':show" class="tinebase-showheaders-link">[...]</span>'
483                 }
484                 
485                 target.innerHTML = html;
486                 
487                 break;
488         }
489     }
490 });