!import
1 //@line 38 "/home/visbrero/mnt/roisin/rev_control/hg/mozilla/mail/extensions/newsblog/content/feed-subscriptions.js"
2
3 const MSG_FOLDER_FLAG_TRASH = 0x0100;
4 const IPS = Components.interfaces.nsIPromptService;
5 const nsIDragService = Components.interfaces.nsIDragService;
6 const kRowIndexUndefined = -1;
7
8 var gFeedSubscriptionsWindow = {
9 mFeedContainers : [],
10 mTree : null,
11 mBundle : null,
12 mRSSServer : null,
13
init
14 init: function ()
15 {
16 // extract the server argument
17 if (window.arguments[0].server)
18 this.mRSSServer = window.arguments[0].server;
19
20 var docshell = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
21 .getInterface(Components.interfaces.nsIWebNavigation)
22 .QueryInterface(Components.interfaces.nsIDocShell);
23 docshell.allowAuth = true;
24
25 this.mTree = document.getElementById("rssSubscriptionsList");
26 this.mBundle = document.getElementById("bundle_newsblog");
27
28 this.loadSubscriptions();
29 this.mTree.treeBoxObject.view = this.mView;
30
31 if (window.arguments[0].folder)
32 this.selectFolder(window.arguments[0].folder);
33 },
34
uninit
35 uninit: function ()
36 {
37 var dismissDialog = true;
38
39 // if we are in the middle of subscribing to a feed, inform the user that
40 // dismissing the dialog right now will abort the feed subscription.
41 // cheat and look at the disabled state of the add button to determine if we are in the middle of a new subscription
42 if (document.getElementById('addFeed').getAttribute('disabled'))
43 {
44 var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].getService(IPS);
45 var newsBlogBundle = document.getElementById("bundle_newsblog");
46 dismissDialog = !(promptService.confirmEx(window, newsBlogBundle.getString('subscribe-cancelSubscriptionTitle'),
47 newsBlogBundle.getString('subscribe-cancelSubscription'),
48 IPS.STD_YES_NO_BUTTONS,
49 null, null, null, null, { }));
50 }
51 return dismissDialog;
52 },
53
54 mView:
55 {
56 mRowCount : 0,
57
get_rowCount
58 get rowCount()
59 {
60 return this.mRowCount;
61 },
62
getItemAtIndex
63 getItemAtIndex: function (aIndex)
64 {
65 if (aIndex < 0 ||
66 aIndex >= gFeedSubscriptionsWindow.mFeedContainers.length)
67 return null;
68 return gFeedSubscriptionsWindow.mFeedContainers[aIndex];
69 },
70
removeItemAtIndex
71 removeItemAtIndex: function (aIndex, aCount)
72 {
73 var itemToRemove = this.getItemAtIndex(aIndex);
74 if (!itemToRemove)
75 return;
76
77 var parentIndex = this.getParentIndex(aIndex);
78 if (parentIndex != kRowIndexUndefined)
79 {
80 var parent = this.getItemAtIndex(parentIndex);
81 if (parent)
82 {
83 for (var index = 0; index < parent.children.length; index++)
84 if (parent.children[index] == itemToRemove)
85 {
86 parent.children.splice(index, 1);
87 break;
88 }
89 }
90 }
91
92 // now remove it from our view
93 gFeedSubscriptionsWindow.mFeedContainers.splice(aIndex, 1);
94
95 // now invalidate the correct tree rows
96 var tbo = gFeedSubscriptionsWindow.mTree.treeBoxObject;
97
98 this.mRowCount--;
99 tbo.rowCountChanged(aIndex, -1);
100
101 // now update the selection position
102 if (aIndex < gFeedSubscriptionsWindow.mFeedContainers.length)
103 this.selection.select(aIndex);
104 else
105 this.selection.clearSelection();
106
107 // now refocus the tree
108 gFeedSubscriptionsWindow.mTree.focus();
109 },
110
getCellText
111 getCellText: function (aIndex, aColumn)
112 {
113 var item = this.getItemAtIndex(aIndex);
114 return (item && aColumn.id == "folderNameCol") ? item.name : "";
115 },
116
117 _selection: null,
get_selection
118 get selection () { return this._selection; },
set_selection
119 set selection (val) { this._selection = val; return val; },
getRowProperties
120 getRowProperties: function (aIndex, aProperties) {},
getCellProperties
121 getCellProperties: function (aIndex, aColumn, aProperties) {},
getColumnProperties
122 getColumnProperties: function (aColumn, aProperties) {},
123
isContainer
124 isContainer: function (aIndex)
125 {
126 var item = this.getItemAtIndex(aIndex);
127 return item ? item.container : false;
128 },
129
isContainerOpen
130 isContainerOpen: function (aIndex)
131 {
132 var item = this.getItemAtIndex(aIndex);
133 return item ? item.open : false;
134 },
135
isContainerEmpty
136 isContainerEmpty: function (aIndex)
137 {
138 var item = this.getItemAtIndex(aIndex);
139 if (!item)
140 return false;
141 return item.children.length == 0;
142 },
143
isSeparator
144 isSeparator: function (aIndex) { return false; },
isSorted
145 isSorted: function (aIndex) { return false; },
146
canDrop
147 canDrop: function (aIndex, aOrientation)
148 {
149 var dropResult = this.extractDragData();
150 return (aOrientation == Components.interfaces.nsITreeView.DROP_ON) &&
151 dropResult.canDrop && (dropResult.url || (dropResult.index != kRowIndexUndefined));
152 },
153
154 mDropUrl: "",
155 mDropFolderUrl: "",
drop
156 drop: function (aIndex, aOrientation)
157 {
158 var results = this.extractDragData();
159 if (!results.canDrop)
160 return;
161
162 if (results.url)
163 {
164 var folderItem = this.getItemAtIndex(aIndex);
165 // don't freeze the app that initiaed the drop just because we are in a loop waiting for the user
166 // to dimisss the add feed dialog....
167 this.mDropUrl = results.url;
168 this.mDropFolderUrl = folderItem.url;
169 setTimeout(processDrop, 0);
170 }
171 else if (results.index != kRowIndexUndefined)
172 gFeedSubscriptionsWindow.moveFeed(results.index, aIndex);
173 },
174
175 // helper function for drag and drop
176 extractDragData: function()
177 {
178 var canDrop = false;
179 var urlToDrop;
180 var sourceIndex = kRowIndexUndefined;
181 var dragService = Components.classes["@mozilla.org/widget/dragservice;1"].getService().QueryInterface(nsIDragService);
182 var dragSession = dragService.getCurrentSession();
183
184 var transfer = Components.classes["@mozilla.org/widget/transferable;1"].createInstance(Components.interfaces.nsITransferable);
185 transfer.addDataFlavor("text/x-moz-url");
186 transfer.addDataFlavor("text/x-moz-feed-index");
187
188 dragSession.getData (transfer, 0);
189 var dataObj = new Object();
190 var flavor = new Object();
191 var len = new Object();
192
193 try {
194 transfer.getAnyTransferData(flavor, dataObj, len);
195 } catch (ex) { return { canDrop: false, url: "" }; }
196
197 if (dataObj.value)
198 {
199 dataObj = dataObj.value.QueryInterface(Components.interfaces.nsISupportsString);
200 sourceUri = dataObj.data.substring(0, len.value); // pull the URL out of the data object
201
202 if (flavor.value == 'text/x-moz-url')
203 {
204 var uri = Components.classes["@mozilla.org/network/standard-url;1"].createInstance(Components.interfaces.nsIURI);
205 uri.spec = sourceUri.split("\n")[0];
206
207 if (uri.schemeIs("http") || uri.schemeIs("https"))
208 {
209 urlToDrop = uri.spec;
210 canDrop = true;
211 }
212 }
213 else if (flavor.value == 'text/x-moz-feed-index')
214 {
215 sourceIndex = parseInt(sourceUri);
216 canDrop = true;
217 }
218 } // if dataObj.value
219
220 return { canDrop: canDrop, url: urlToDrop, index: sourceIndex };
221 },
222
getParentIndex
223 getParentIndex: function (aIndex)
224 {
225 var item = this.getItemAtIndex(aIndex);
226
227 if (item)
228 {
229 for (var index = aIndex; index >= 0; index--)
230 if (gFeedSubscriptionsWindow.mFeedContainers[index].level < item.level)
231 return index;
232 }
233
234 return kRowIndexUndefined;
235 },
hasNextSibling
236 hasNextSibling: function (aParentIndex, aIndex)
237 {
238 var item = this.getItemAtIndex(aIndex);
239 if (item)
240 {
241 // if the next node in the view has the same level as us, then we must have a next sibling...
242 if (aIndex + 1 < gFeedSubscriptionsWindow.mFeedContainers.length )
243 return this.getItemAtIndex(aIndex + 1).level == item.level;
244 }
245
246 return false;
247 },
hasPreviousSibling
248 hasPreviousSibling: function (aIndex)
249 {
250 var item = this.getItemAtIndex(aIndex);
251 if (item && aIndex)
252 return this.getItemAtIndex(aIndex - 1).level == item.level;
253 else
254 return false;
255 },
getLevel
256 getLevel: function (aIndex)
257 {
258 var item = this.getItemAtIndex(aIndex);
259 if (!item)
260 return 0;
261 return item.level;
262 },
getImageSrc
263 getImageSrc: function (aIndex, aColumn) {},
getProgressMode
264 getProgressMode: function (aIndex, aColumn) {},
getCellValue
265 getCellValue: function (aIndex, aColumn) {},
setTree
266 setTree: function (aTree) {},
toggleOpenState
267 toggleOpenState: function (aIndex)
268 {
269 var item = this.getItemAtIndex(aIndex);
270 if (!item) return;
271
272 // save off the current selection item
273 var seln = this.selection;
274 var currentSelectionIndex = seln.currentIndex;
275
276 var multiplier = item.open ? -1 : 1;
277 var delta = multiplier * item.children.length;
278 this.mRowCount += delta;
279
280 if (multiplier < 0)
281 gFeedSubscriptionsWindow.mFeedContainers.splice(aIndex + 1, item.children.length);
282 else
283 for (var i = 0; i < item.children.length; i++)
284 gFeedSubscriptionsWindow.mFeedContainers.splice(aIndex + 1 + i, 0, item.children[i]);
285
286 // add or remove the children from our view
287 item.open = !item.open;
288 gFeedSubscriptionsWindow.mTree.treeBoxObject.rowCountChanged(aIndex, delta);
289
290 // now restore selection
291 seln.select(currentSelectionIndex);
292
293 },
294 cycleHeader: function (aColumn) {},
selectionChanged
295 selectionChanged: function () {},
cycleCell
296 cycleCell: function (aIndex, aColumn) {},
isEditable
297 isEditable: function (aIndex, aColumn)
298 {
299 return false;
300 },
isSelectable
301 isSelectable: function (aIndex, aColumn)
302 {
303 return false;
304 },
setCellValue
305 setCellValue: function (aIndex, aColumn, aValue) {},
setCellText
306 setCellText: function (aIndex, aColumn, aValue) {},
performAction
307 performAction: function (aAction) {},
performActionOnRow
308 performActionOnRow: function (aAction, aIndex) {},
performActionOnCell
309 performActionOnCell: function (aAction, aindex, aColumn) {}
310 },
311
makeFolderObject
312 makeFolderObject: function (aFolder, aCurrentLevel)
313 {
314 var folderObject = { children : [],
315 name : aFolder.prettiestName,
316 level : aCurrentLevel,
317 url : aFolder.QueryInterface(Components.interfaces.nsIRDFResource).Value,
318 open : false,
319 container: true };
320
321 // if a feed has any sub folders, we should add them to the list of children
322 if (aFolder.hasSubFolders)
323 {
324 var folderEnumerator = aFolder.subFoldersObsolete;
325 var done = false;
326
327 while (!done)
328 {
329 var folder = folderEnumerator.currentItem().QueryInterface(Components.interfaces.nsIMsgFolder);
330 folderObject.children.push(this.makeFolderObject(folder, aCurrentLevel + 1));
331
332 try {
333 folderEnumerator.next();
334 }
335 catch (ex)
336 {
337 done = true;
338 }
339 }
340 }
341
342 var feeds = this.getFeedsInFolder(aFolder);
343
344 for (feed in feeds)
345 {
346 // Special case, if a folder only has a single feed associated with it, then just use the feed
347 // in the view and don't show the folder at all.
348 // if (feedUrlArray.length <= 2 && !aFolder.hasSubFolders) // Note: split always adds an empty element to the array...
349 // this.mFeedContainers[aCurrentLength] = this.makeFeedObject(feed, aCurrentLevel);
350 // else // now add any feed urls for the folder
351 folderObject.children.push(this.makeFeedObject(feeds[feed], aCurrentLevel + 1));
352 }
353
354 return folderObject;
355 },
356
getFeedsInFolder
357 getFeedsInFolder: function (aFolder)
358 {
359 var feeds = new Array();
360 try
361 {
362 var msgdb = aFolder.QueryInterface(Components.interfaces.nsIMsgFolder).getMsgDatabase(null);
363 var folderInfo = msgdb.dBFolderInfo;
364 var feedurls = folderInfo.getCharProperty("feedUrl");
365 var feedUrlArray = feedurls.split("|");
366 for (url in feedUrlArray)
367 {
368 if (!feedUrlArray[url])
369 continue;
370 var feedResource = rdf.GetResource(feedUrlArray[url]);
371 var feed = new Feed(feedResource, this.mRSSServer);
372 feeds.push(feed);
373 }
374 }
375 catch(ex) {}
376 return feeds;
377 },
378
makeFeedObject
379 makeFeedObject: function (aFeed, aLevel)
380 {
381 // look inside the data source for the feed properties
382 var feed = { children : [],
383 name : aFeed.title,
384 url : aFeed.url,
385 level : aLevel,
386 open : false,
387 container : false };
388 return feed;
389 },
390
loadSubscriptions
391 loadSubscriptions: function ()
392 {
393 // put together an array of folders
394 var numFolders = 0;
395 this.mFeedContainers = [];
396
397 if (this.mRSSServer.rootFolder.hasSubFolders)
398 {
399 var folderEnumerator = this.mRSSServer.rootFolder.subFoldersObsolete;
400 var done = false;
401
402 while (!done)
403 {
404 var folder = folderEnumerator.currentItem().QueryInterface(Components.interfaces.nsIMsgFolder);
405 if (folder && !folder.getFlag(MSG_FOLDER_FLAG_TRASH))
406 {
407 this.mFeedContainers.push(this.makeFolderObject(folder, 0));
408 numFolders++;
409 }
410
411 try {
412 folderEnumerator.next();
413 }
414 catch (ex)
415 {
416 done = true;
417 }
418 }
419 }
420 this.mView.mRowCount = numFolders;
421
422 gFeedSubscriptionsWindow.mTree.focus();
423 },
424
selectFolder
425 selectFolder: function(aFolder)
426 {
427 if (aFolder.isServer)
428 return;
429
430 var folderURI = aFolder.QueryInterface(Components.interfaces.nsIRDFResource)
431 .Value;
432
containsFolder
433 function containsFolder(aItem)
434 {
435 var items = aItem ? aItem.children : this.mFeedContainers;
436 for (var i = 0; i < items.length; i++) {
437 if (items[i].url == folderURI ||
438 item.container && containsFolder(items[i]))
439 return true;
440 }
441 return false;
442 }
443
444 for (var index = 0; index < this.mView.rowCount; index++)
445 {
446 var item = this.mView.getItemAtIndex(index);
447 if (item.url == folderURI || containsFolder(item))
448 {
449 if (!item.open)
450 this.mView.toggleOpenState(index);
451 if (item.url == folderURI) {
452 // we found the actual folder - not an ancestor
453 this.mTree.view.selection.select(index);
454 this.mTree.boxObject.ensureRowIsVisible(index);
455 break;
456 }
457 }
458 }
459 },
460
selectFeed
461 selectFeed: function(aFeed)
462 {
463 this.selectFolder(aFeed.folder);
464
465 var seln = this.mTree.view.selection;
466 var item = this.mView.getItemAtIndex(seln.currentIndex);
467 if (item) {
468 for (var i = 0; i < item.children.length; i++) {
469 if (item.children[i].url == aFeed.url) {
470 var index = seln.currentIndex + i + 1;
471 this.mTree.view.selection.select(index);
472 this.mTree.boxObject.ensureRowIsVisible(index);
473 break;
474 }
475 }
476 }
477 },
478
updateFeedData
479 updateFeedData: function (aItem)
480 {
481 var ids = ['nameLabel', 'nameValue', 'locationLabel', 'locationValue'];
482 if (aItem && !aItem.container)
483 {
484 // set the feed location and title info
485 document.getElementById('nameValue').value = aItem.name;
486 document.getElementById('locationValue').value = aItem.url;
487 }
488 else
489 {
490 var noneSelected = this.mBundle.getString("subscribe-noFeedSelected");
491 document.getElementById('nameValue').value = noneSelected;
492 document.getElementById('locationValue').value = "";
493 }
494
495 for (var i = 0; i < ids.length; ++i)
496 document.getElementById(ids[i]).disabled = !aItem || aItem.container;
497 },
498
onKeyPress
499 onKeyPress: function(aEvent)
500 {
501 if (aEvent.keyCode == aEvent.DOM_VK_ENTER || aEvent.keyCode == aEvent.DOM_VK_RETURN)
502 {
503 var seln = this.mTree.view.selection;
504 item = this.mView.getItemAtIndex(seln.currentIndex);
505 if (item && !item.container)
506 this.editFeed();
507 }
508 },
509
onSelect
510 onSelect: function ()
511 {
512 var properties, item;
513 var seln = this.mTree.view.selection;
514 item = this.mView.getItemAtIndex(seln.currentIndex);
515
516 this.updateFeedData(item);
517
518 document.getElementById("removeFeed").disabled = !item || item.container;
519 document.getElementById("editFeed").disabled = !item || item.container;
520 },
521
removeFeed
522 removeFeed: function ()
523 {
524 var seln = this.mView.selection;
525 if (seln.count != 1) return;
526
527 var itemToRemove = this.mView.getItemAtIndex(seln.currentIndex);
528
529 if (!itemToRemove)
530 return;
531
532 // ask the user if he really wants to unsubscribe from the feed
533 var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].getService(IPS);
534 var abortRemoval = promptService.confirmEx(window, this.mBundle.getString('subsribe-confirmFeedDeletionTitle'),
535 this.mBundle.getFormattedString('subsribe-confirmFeedDeletion', [itemToRemove.name], 1),
536 IPS.STD_YES_NO_BUTTONS, null, null, null, null, { });
537 if (abortRemoval)
538 return;
539
540 var resource = rdf.GetResource(itemToRemove.url);
541 var feed = new Feed(resource, this.mRSSServer);
542 var ds = getSubscriptionsDS(this.mRSSServer);
543
544 if (feed && ds)
545 {
546 // remove the feed from the subscriptions ds
547 var feeds = getSubscriptionsList(this.mRSSServer);
548 var index = feeds.IndexOf(resource);
549 if (index != kRowIndexUndefined)
550 feeds.RemoveElementAt(index, false);
551
552 // remove the feed property string from the folder data base
553 var currentFolder = ds.GetTarget(resource, FZ_DESTFOLDER, true);
554 if (currentFolder)
555 {
556 var currentFolderURI = currentFolder.QueryInterface(Components.interfaces.nsIRDFResource).Value;
557 currentFolder = rdf.GetResource(currentFolderURI).QueryInterface(Components.interfaces.nsIMsgFolder);
558
559 var feedUrl = ds.GetTarget(resource, DC_IDENTIFIER, true);
560 ds.Unassert(resource, DC_IDENTIFIER, feedUrl, true);
561
562 feedUrl = feedUrl ? feedUrl.QueryInterface(Components.interfaces.nsIRDFLiteral).Value : "";
563
564 updateFolderFeedUrl(currentFolder, feedUrl, true); // remove the old url
565 }
566
567 // Remove all assertions about the feed from the subscriptions database.
568 removeAssertions(ds, resource);
569 ds.QueryInterface(Components.interfaces.nsIRDFRemoteDataSource).Flush(); // flush any changes
570
571 // Remove all assertions about items in the feed from the items database.
572 var itemds = getItemsDS(this.mRSSServer);
573 feed.invalidateItems();
574 feed.removeInvalidItems();
575 itemds.QueryInterface(Components.interfaces.nsIRDFRemoteDataSource).Flush(); // flush any changes
576 }
577
578 // Now that we have removed the feed from the datasource, it is time to update our
579 // view layer. Start by removing the child from its parent folder object
580 this.mView.removeItemAtIndex(seln.currentIndex);
581
582 // If we don't have any more subscriptions pointing into
583 // this folder, then I think we should offer to delete it...
584 // Cheat and look at the feed url property to see if anyone else is still using the feed...
585 // you could also accomplish this by looking at some properties in the data source...
586
587 // var msgdb = currentFolder.QueryInterface(Components.interfaces.nsIMsgFolder).getMsgDatabase(null);
588 // var folderInfo = msgdb.dBFolderInfo;
589 // var oldFeedUrl = folderInfo.getCharProperty("feedUrl");
590
591 // if (!oldFeedUrl) // no more feeds pointing to the folder?
592 // {
593 // try {
594 // var openerResource = this.mRSSServer.rootMsgFolder.QueryInterface(Components.interfaces.nsIRDFResource);
595 // var folderResource = currentFolder.QueryInterface(Components.interfaces.nsIRDFResource);
596 // window.opener.messenger.DeleteFolders(window.opener.GetFolderDatasource(), openerResource, folderResource);
597 // } catch (e) { }
598 // }
599 },
600
601 // aRootFolderURI --> optional argument. The folder to initially create the new feed under.
addFeed
602 addFeed: function(aFeedLocation, aRootFolderURI)
603 {
604 var userAddedFeed = false;
605 var defaultQuickMode = this.mRSSServer.getBoolAttribute('quickMode');
606 var feedProperties = { feedName: "", feedLocation: aFeedLocation,
607 serverURI: this.mRSSServer.serverURI,
608 serverPrettyName: this.mRSSServer.prettyName,
609 quickMode: this.mRSSServer.getBoolAttribute('quickMode'),
610 newFeed: true,
611 result: userAddedFeed};
612
613 // unless another folder is specified, default to currently selected
614 if (aRootFolderURI) {
615 feedProperties.folderURI = aRootFolderURI;
616 } else {
617 var index = this.mTree.view.selection.currentIndex;
618 var item = this.mView.getItemAtIndex(index);
619 if (item) {
620 if (!item.container)
621 item = this.mView.getItemAtIndex(this.mView.getParentIndex(index));
622 feedProperties.folderURI = item.url;
623 }
624 }
625
626 feedProperties = openFeedEditor(feedProperties);
627
628 // if the user hit cancel, exit without doing anything
629 if (!feedProperties.result)
630 return;
631
632 if (!feedProperties.feedLocation)
633 return;
634
635 // before we go any further, make sure the user is not already subscribed to this feed.
636 if (feedAlreadyExists(feedProperties.feedLocation, this.mRSSServer))
637 {
638 var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].getService(IPS);
639 promptService.alert(window, null, this.mBundle.getString("subscribe-feedAlreadySubscribed"));
640 return;
641 }
642
643 var feed = this.storeFeed(feedProperties);
644 if(!feed)
645 return;
646
647 // Now validate and start downloadng the feed....
648 updateStatusItem('statusText', document.getElementById("bundle_newsblog").getString('subscribe-validating'));
649 updateStatusItem('progressMeter', 0);
650 document.getElementById('addFeed').setAttribute('disabled', 'true');
651 feed.download(true, this.mFeedDownloadCallback);
652 },
653
654 // helper routine used by addFeed and importOPMLFile
storeFeed
655 storeFeed: function(feedProperties)
656 {
657 var itemResource = rdf.GetResource(feedProperties.feedLocation);
658 var feed = new Feed(itemResource, this.mRSSServer);
659
660 // if the user specified a specific folder to add the feed too, then set it here
661 if (feedProperties.folderURI)
662 {
663 var folderResource = rdf.GetResource(feedProperties.folderURI);
664 if (folderResource)
665 {
666 var folder = folderResource.QueryInterface(Components.interfaces.nsIMsgFolder);
667 if (folder && !folder.isServer)
668 feed.folder = folder;
669 }
670 }
671
672 feed.quickMode = feedProperties.quickMode;
673 return feed;
674 },
675
editFeed
676 editFeed: function()
677 {
678 var seln = this.mView.selection;
679 if (seln.count != 1)
680 return;
681
682 var itemToEdit = this.mView.getItemAtIndex(seln.currentIndex);
683 if (!itemToEdit || itemToEdit.container)
684 return;
685
686 var resource = rdf.GetResource(itemToEdit.url);
687 var feed = new Feed(resource, this.mRSSServer);
688
689 var ds = getSubscriptionsDS(this.mRSSServer);
690 var currentFolder = ds.GetTarget(resource, FZ_DESTFOLDER, true);
691 var currentFolderURI = currentFolder.QueryInterface(Components.interfaces.nsIRDFResource).Value;
692
693 var userModifiedFeed = false;
694 var feedProperties = { feedLocation: itemToEdit.url, serverURI: this.mRSSServer.serverURI,
695 serverPrettyName: this.mRSSServer.prettyName, folderURI: currentFolderURI,
696 quickMode: feed.quickMode, newFeed: false, result: userModifiedFeed};
697
698 feedProperties = openFeedEditor(feedProperties);
699 if (!feedProperties.result) // did the user cancel?
700 return;
701
702 // check to see if the quickMode value changed
703 if (feed.quickMode != feedProperties.quickMode)
704 feed.quickMode = feedProperties.quickMode;
705
706 // did the user change the folder URI for storing the feed?
707 if (feedProperties.folderURI && feedProperties.folderURI != currentFolderURI)
708 {
709 // we need to find the index of the new parent folder...
710 var newParentIndex = kRowIndexUndefined;
711 for (index = 0; index < this.mView.rowCount; index++)
712 {
713 var item = this.mView.getItemAtIndex(index);
714 if (item && item.container && item.url == feedProperties.folderURI)
715 {
716 newParentIndex = index;
717 break;
718 }
719 }
720
721 if (newParentIndex != kRowIndexUndefined)
722 this.moveFeed(seln.currentIndex, newParentIndex)
723 }
724
725 ds.QueryInterface(Components.interfaces.nsIRDFRemoteDataSource).Flush(); // flush any changes
726 },
727
728 // moves the feed located at aOldFeedIndex to a child of aNewParentIndex
moveFeed
729 moveFeed: function(aOldFeedIndex, aNewParentIndex)
730 {
731 // if the new parent is the same as the current parent, then do nothing
732 if (this.mView.getParentIndex(aOldFeedIndex) == aNewParentIndex)
733 return;
734
735 var currentItem = this.mView.getItemAtIndex(aOldFeedIndex);
736 var currentParentItem = this.mView.getItemAtIndex(this.mView.getParentIndex(aOldFeedIndex));
737 var currentParentResource = rdf.GetResource(currentParentItem.url);
738
739 var newParentItem = this.mView.getItemAtIndex(aNewParentIndex);
740 var newParentResource = rdf.GetResource(newParentItem.url);
741
742 var ds = getSubscriptionsDS(this.mRSSServer);
743 var resource = rdf.GetResource(currentItem.url);
744 var currentFolder = currentParentResource.QueryInterface(Components.interfaces.nsIMsgFolder);
745
746 // unassert the older URI, add an assertion for the new parent URI...
747 ds.Change(resource, FZ_DESTFOLDER, currentParentResource, newParentResource);
748
749 // we need to update the feed url attributes on the databases for each folder
750 updateFolderFeedUrl(currentParentResource.QueryInterface(Components.interfaces.nsIMsgFolder),
751 currentItem.url, true); // remove our feed url property from the current folder
752 updateFolderFeedUrl(newParentResource.QueryInterface(Components.interfaces.nsIMsgFolder),
753 currentItem.url, false); // add our feed url property to the new folder
754
755
756 // Finally, update our view layer
757 this.mView.removeItemAtIndex(aOldFeedIndex, 1);
758 if (aNewParentIndex > aOldFeedIndex)
759 aNewParentIndex--;
760
761 currentItem.level = newParentItem.level + 1;
762 newParentItem.children.push(currentItem);
763 var indexOfNewItem = aNewParentIndex + newParentItem.children.length;
764
765 if (!newParentItem.open) // force open the container
766 this.mView.toggleOpenState(aNewParentIndex);
767 else
768 {
769 this.mFeedContainers.splice(indexOfNewItem, 0, currentItem);
770 this.mView.mRowCount++;
771 this.mTree.treeBoxObject.rowCountChanged(indexOfNewItem, 1);
772 }
773
774 gFeedSubscriptionsWindow.mTree.view.selection.select(indexOfNewItem)
775 },
776
beginDrag
777 beginDrag: function (aEvent)
778 {
779 // get the selected feed article (if there is one)
780 var seln = this.mView.selection;
781 if (seln.count != 1)
782 return;
783
784 // only initiate a drag if the item is a feed (i.e. ignore folders/containers)
785 var item = this.mView.getItemAtIndex(seln.currentIndex);
786 if (!item || item.container)
787 return;
788
789 var transfer = Components.classes["@mozilla.org/widget/transferable;1"].createInstance(Components.interfaces.nsITransferable);
790 var transArray = Components.classes["@mozilla.org/supports-array;1"].createInstance(Components.interfaces.nsISupportsArray);
791 var dragData = Components.classes["@mozilla.org/supports-string;1"].createInstance(Components.interfaces.nsISupportsString);
792
793 transfer.addDataFlavor("text/x-moz-feed-index"); // i made this flavor type up
794 dragData.data = seln.currentIndex.toString();
795
796 transfer.setTransferData ( "text/x-moz-feed-index", dragData, seln.currentIndex.toString() * 2 ); // doublebyte byte data
797 transArray.AppendElement(transfer.QueryInterface(Components.interfaces.nsISupports));
798
799 var dragService = Components.classes["@mozilla.org/widget/dragservice;1"].getService().QueryInterface(nsIDragService);
800 dragService.invokeDragSession ( aEvent.target, transArray, null, nsIDragService.DRAGDROP_ACTION_MOVE);
801 },
802
803 mFeedDownloadCallback:
804 {
downloaded
805 downloaded: function(feed, aErrorCode)
806 {
807 // feed is null if our attempt to parse the feed failed
808 if (aErrorCode == kNewsBlogSuccess)
809 {
810 updateStatusItem('progressMeter', 100);
811
812 // if we get here...we should always have a folder by now...either
813 // in feed.folder or FeedItems created the folder for us....
814 updateFolderFeedUrl(feed.folder, feed.url, false);
815
816 // add feed just adds the feed we have validated and downloaded to our datasource
817 // it also flushes the subscription datasource
818 addFeed(feed.url, feed.name, feed.folder);
819
820 // now add the feed to our view
821 refreshSubscriptionView();
822
823 gFeedSubscriptionsWindow.selectFeed(feed);
824 }
825 else if (aErrorCode == kNewsBlogInvalidFeed) // the feed was bad...
826 window.alert(gFeedSubscriptionsWindow.mBundle.getFormattedString('newsblog-invalidFeed', [feed.url]));
827 else if (aErrorCode == kNewsBlogRequestFailure)
828 window.alert(gFeedSubscriptionsWindow.mBundle.getFormattedString('newsblog-networkError', [feed.url]));
829
830 // re-enable the add button now that we are done subscribing
831 document.getElementById('addFeed').removeAttribute('disabled');
832
833 // our operation is done...clear out the status text and progressmeter
834 setTimeout(clearStatusInfo, 1000);
835 },
836
837 // this gets called after the RSS parser finishes storing a feed item to disk
838 // aCurrentFeedItems is an integer corresponding to how many feed items have been downloaded so far
839 // aMaxFeedItems is an integer corresponding to the total number of feed items to download
onFeedItemStored
840 onFeedItemStored: function (feed, aCurrentFeedItems, aMaxFeedItems)
841 {
842 updateStatusItem('statusText', gFeedSubscriptionsWindow.mBundle.getFormattedString("subscribe-fetchingFeedItems",
843 [aCurrentFeedItems, aMaxFeedItems]));
844 this.onProgress(feed, aCurrentFeedItems, aMaxFeedItems);
845 },
846
onProgress
847 onProgress: function(feed, aProgress, aProgressMax)
848 {
849 updateStatusItem('progressMeter', (aProgress * 100) / aProgressMax);
850 }
851 },
852
exportOPML
853 exportOPML: function()
854 {
855 if (this.mRSSServer.rootFolder.hasSubFolders)
856 {
857 var opmlDoc = document.implementation.createDocument("","opml",null);
858 var opmlRoot = opmlDoc.documentElement;
859 opmlRoot.setAttribute("version","1.0");
860
861 this.generatePPSpace(opmlRoot," ");
862
863 // Make the <head> element
864 var head = opmlDoc.createElement("head");
865 this.generatePPSpace(head, " ");
866 var title = opmlDoc.createElement("title");
867 title.appendChild(opmlDoc.createTextNode(this.mBundle.getString("subscribe-OPMLExportFileTitle")));
868 head.appendChild(title);
869 this.generatePPSpace(head, " ");
870 var dt = opmlDoc.createElement("dateCreated");
871 dt.appendChild(opmlDoc.createTextNode((new Date()).toGMTString()));
872 head.appendChild(dt);
873 this.generatePPSpace(head, " ");
874 opmlRoot.appendChild(head);
875
876 this.generatePPSpace(opmlRoot, " ");
877
878 //add <outline>s to the <body>
879 var body = opmlDoc.createElement("body");
880 this.generateOutline(this.mRSSServer.rootFolder, body, 4);
881 this.generatePPSpace(body, " ");
882 opmlRoot.appendChild(body);
883
884 this.generatePPSpace(opmlRoot, "");
885
886 var serial=new XMLSerializer();
887 var rv = pickSaveAs(this.mBundle.getString("subscribe-OPMLExportTitle"),'$all',
888 this.mBundle.getString("subscribe-OPMLExportFileName"));
889 if(rv.reason == PICK_CANCEL)
890 return;
891 else if(rv)
892 {
893 //debug("opml:\n"+serial.serializeToString(opmlDoc)+"\n");
894 var file = new LocalFile(rv.file, MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE);
895 serial.serializeToStream(opmlDoc,file.outputStream,'utf-8');
896 file.close();
897 }
898 }
899 },
900
generatePPSpace
901 generatePPSpace: function(aNode, indentString)
902 {
903 aNode.appendChild(aNode.ownerDocument.createTextNode("\n"));
904 aNode.appendChild(aNode.ownerDocument.createTextNode(indentString));
905 },
906
generateOutline
907 generateOutline: function(baseFolder, parent, indentLevel)
908 {
909 var folderEnumerator = baseFolder.subFoldersObsolete;
910 var done = false;
911
912 // pretty printing
913 var indentString = "";
914 for(i = 0; i < indentLevel; i++)
915 indentString = indentString + " ";
916
917 while (!done)
918 {
919 var folder = folderEnumerator.currentItem().QueryInterface(Components.interfaces.nsIMsgFolder);
920 if (folder && !folder.getFlag(MSG_FOLDER_FLAG_TRASH))
921 {
922 var outline;
923 if(folder.hasSubFolders)
924 {
925 // Make a mostly empty outline element
926 outline = parent.ownerDocument.createElement("outline");
927 outline.setAttribute("text",folder.prettiestName);
928 this.generateOutline(folder, outline, indentLevel+2); // recurse
929 this.generatePPSpace(parent, indentString);
930 this.generatePPSpace(outline, indentString);
931 parent.appendChild(outline);
932 }
933 else
934 {
935 // Add outline elements with xmlUrls
936 var feeds = this.getFeedsInFolder(folder);
937 for (feed in feeds)
938 {
939 outline = this.opmlFeedToOutline(feeds[feed],parent.ownerDocument);
940 this.generatePPSpace(parent, indentString);
941 parent.appendChild(outline);
942 }
943 }
944 }
945
946 try {
947 folderEnumerator.next();
948 }
949 catch (ex)
950 {
951 done = true;
952 }
953 }
954 },
955
opmlFeedToOutline
956 opmlFeedToOutline: function(aFeed,aDoc)
957 {
958 var outRv = aDoc.createElement("outline");
959 outRv.setAttribute("title",aFeed.title);
960 outRv.setAttribute("text",aFeed.title);
961 outRv.setAttribute("type","rss");
962 outRv.setAttribute("version","RSS");
963 outRv.setAttribute("xmlUrl",aFeed.url);
964 outRv.setAttribute("htmlUrl",aFeed.link);
965 return outRv;
966 },
967
importOPML
968 importOPML: function()
969 {
970 var rv = pickOpen(this.mBundle.getString("subscribe-OPMLImportTitle"), '$xml $opml $all');
971 if(rv.reason == PICK_CANCEL)
972 return;
973
974 var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].getService(IPS);
975 var stream = Components.classes["@mozilla.org/network/file-input-stream;1"].createInstance(Components.interfaces.nsIFileInputStream);
976 var opmlDom = null;
977
978 // read in file as raw bytes, so Expat can do the decoding for us
979 try{
980 stream.init(rv.file, MODE_RDONLY, PERM_IROTH, 0);
981 var parser = new DOMParser();
982 opmlDom = parser.parseFromStream(stream, null, stream.available(), 'application/xml');
983 }catch(e){
984 promptService.alert(window, null, this.mBundle.getString("subscribe-errorOpeningFile"));
985 return;
986 }finally{
987 stream.close();
988 }
989
990 // return if the user didn't give us an OPML file
991 if(!opmlDom || !(opmlDom.documentElement.tagName == "opml"))
992 {
993 promptService.alert(window, null, this.mBundle.getFormattedString("subscribe-errorInvalidOPMLFile", [rv.file.leafName]));
994 return;
995 }
996
997 var outlines = opmlDom.getElementsByTagName("body")[0].getElementsByTagName("outline");
998 var feedsAdded = false;
999
1000 for (var index = 0; index < outlines.length; index++)
1001 {
1002 var outline = outlines[index];
1003
1004 // XXX only dealing with flat OPML files for now.
1005 // We still need to add support for grouped files.
1006 if(outline.hasAttribute("xmlUrl") || outline.hasAttribute("url"))
1007 {
1008 var userAddedFeed = false;
1009 var newFeedUrl = outline.getAttribute("xmlUrl") || outline.getAttribute("url")
1010 var defaultQuickMode = this.mRSSServer.getBoolAttribute('quickMode');
1011 var feedProperties = { feedName: this.findOutlineTitle(outline),
1012 feedLocation: newFeedUrl,
1013 serverURI: this.mRSSServer.serverURI,
1014 serverPrettyName: this.mRSSServer.prettyName,
1015 folderURI: "",
1016 quickMode: this.mRSSServer.getBoolAttribute('quickMode')};
1017
1018 debug("importing feed: "+ feedProperties.feedName);
1019
1020 // Silently skip feeds that are already subscribed to.
1021 if (!feedAlreadyExists(feedProperties.feedLocation, this.mRSSServer))
1022 {
1023 var feed = this.storeFeed(feedProperties);
1024
1025 if(feed)
1026 {
1027 feed.title = feedProperties.feedName;
1028 if(outline.hasAttribute("htmlUrl"))
1029 feed.link = outline.getAttribute("htmlUrl");
1030
1031 feed.createFolder();
1032 updateFolderFeedUrl(feed.folder, feed.url, false);
1033
1034 // add feed adds the feed we have validated and downloaded to our datasource
1035 // it also flushes the subscription datasource
1036 addFeed(feed.url, feed.name, feed.folder);
1037 feedsAdded = true;
1038 }
1039 }
1040 }
1041 }
1042
1043 if (!outlines.length || !feedsAdded)
1044 {
1045 promptService.alert(window, null, this.mBundle.getFormattedString("subscribe-errorInvalidOPMLFile", [rv.file.leafName]));
1046 return;
1047 }
1048
1049 //add the new feeds to our view
1050 refreshSubscriptionView();
1051 },
1052
findOutlineTitle
1053 findOutlineTitle: function(anOutline)
1054 {
1055 var outlineTitle;
1056
1057 if (anOutline.hasAttribute("text"))
1058 outlineTitle = anOutline.getAttribute("text");
1059 else if (anOutline.hasAttribute("title"))
1060 outlineTitle = anOutline.getAttribute("title");
1061 else if (anOutline.hasAttribute("xmlUrl"))
1062 outlineTitle = anOutline.getAttribute("xmlUrl");
1063
1064 return outlineTitle;
1065 }
1066 };
1067
1068 // opens the feed properties dialog
openFeedEditor
1069 function openFeedEditor(aFeedProperties)
1070 {
1071 window.openDialog('chrome://messenger-newsblog/content/feed-properties.xul', 'feedproperties', 'modal,titlebar,chrome,center', aFeedProperties);
1072 return aFeedProperties;
1073 }
1074
refreshSubscriptionView
1075 function refreshSubscriptionView()
1076 {
1077 gFeedSubscriptionsWindow.loadSubscriptions();
1078 gFeedSubscriptionsWindow.mTree.treeBoxObject.invalidate();
1079 gFeedSubscriptionsWindow.mTree.treeBoxObject.view = gFeedSubscriptionsWindow.mView;
1080 if (gFeedSubscriptionsWindow.mView.rowCount > 0)
1081 gFeedSubscriptionsWindow.mTree.view.selection.select(0);
1082 }
1083
processDrop
1084 function processDrop()
1085 {
1086 gFeedSubscriptionsWindow.addFeed(gFeedSubscriptionsWindow.mView.mDropUrl, gFeedSubscriptionsWindow.mView.mDropFolderUrl);
1087 }
1088
1089 // status helper routines
1090
updateStatusItem
1091 function updateStatusItem(aID, aValue)
1092 {
1093 var el = document.getElementById(aID);
1094 if (el.getAttribute('collapsed'))
1095 el.removeAttribute('collapsed');
1096
1097 el.value = aValue;
1098 }
1099
clearStatusInfo
1100 function clearStatusInfo()
1101 {
1102 document.getElementById('statusText').value = "";
1103 document.getElementById('progressMeter').collapsed = true;
1104 }