зеркало из https://github.com/mozilla/pjs.git
1847 строки
48 KiB
C++
1847 строки
48 KiB
C++
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
|
|
*
|
|
* The contents of this file are subject to the Netscape Public License
|
|
* Version 1.0 (the "NPL"); you may not use this file except in
|
|
* compliance with the NPL. You may obtain a copy of the NPL at
|
|
* http://www.mozilla.org/NPL/
|
|
*
|
|
* Software distributed under the NPL is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the NPL
|
|
* for the specific language governing rights and limitations under the
|
|
* NPL.
|
|
*
|
|
* The Initial Developer of this code under the NPL is Netscape
|
|
* Communications Corporation. Portions created by Netscape are
|
|
* Copyright (C) 1998 Netscape Communications Corporation. All Rights
|
|
* Reserved.
|
|
*/
|
|
|
|
#include "msg.h"
|
|
#include "errcode.h"
|
|
|
|
#include "subpane.h"
|
|
#include "hosttbl.h"
|
|
#include "msgmast.h"
|
|
#include "msgfinfo.h"
|
|
#include "xp_hash.h"
|
|
#include "newshost.h"
|
|
#include "imaphost.h"
|
|
#include "xpgetstr.h"
|
|
#include "grec.h"
|
|
#include "xp_qsort.h"
|
|
#include "msgurlq.h"
|
|
|
|
extern "C"
|
|
{
|
|
extern int MK_OUT_OF_MEMORY;
|
|
extern int MK_MSG_SEARCH_FAILED;
|
|
extern int MK_MSG_INCOMPLETE_NEWSGROUP_LIST;
|
|
extern int XP_ALERT_OFFLINE_MODE_SELECTED;
|
|
extern int MK_NNTP_SERVER_NOT_CONFIGURED;
|
|
}
|
|
|
|
// we are using the same ChangeSubscribe structure for both
|
|
// News and IMAP. We are also keeping these structs in the same hash
|
|
struct ChangeSubscribe {
|
|
MSG_Host* host;
|
|
char* groupname;
|
|
XP_Bool subscribe; // Whether we're trying to subscribe or
|
|
// unsubscribe to this group.
|
|
ChangeSubscribe* next;
|
|
};
|
|
|
|
static XP_Bool imap_freehashkeys(XP_HashTable table, const void* key,
|
|
void* value, void* closure);
|
|
|
|
MSG_SubscribePane* MSG_SubscribePane::CurPane = NULL;
|
|
|
|
|
|
MSG_SubscribePane*
|
|
MSG_SubscribePane::Create(MWContext* context, MSG_Master* master,
|
|
MSG_Host* host)
|
|
{
|
|
XP_ASSERT(!CurPane);
|
|
if (CurPane) return NULL;
|
|
|
|
MSG_Host* defhost = NULL;
|
|
|
|
// first try getting the default News (NNTP) host
|
|
defhost = MSG_GetDefaultNewsHost(master);
|
|
|
|
// if no NNTP host, try getting the default IMAP host
|
|
if (!defhost)
|
|
{
|
|
defhost = MSG_GetDefaultIMAPHost(master);
|
|
}
|
|
|
|
// no hosts configured
|
|
if (defhost == NULL)
|
|
{
|
|
FE_Alert(context, XP_GetString(MK_NNTP_SERVER_NOT_CONFIGURED));
|
|
#ifdef XP_MAC
|
|
/* AFTER the alert, not before! */
|
|
FE_EditPreference(PREF_NewsHost);
|
|
#endif
|
|
return NULL;
|
|
}
|
|
|
|
MSG_SubscribePane* result = new MSG_SubscribePane(context, master, host);
|
|
if (result && CurPane == NULL) {
|
|
// We ran out of memory or something. Abort.
|
|
delete result;
|
|
result = NULL;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
HashComp(const void *obj1, const void *obj2)
|
|
{
|
|
XP_ASSERT(obj1 && obj2);
|
|
ChangeSubscribe* c1 = (ChangeSubscribe*) obj1;
|
|
ChangeSubscribe* c2 = (ChangeSubscribe*) obj2;
|
|
int delta = XP_STRCMP(c1->groupname, c2->groupname);
|
|
if (delta) return delta;
|
|
if (c1->host < c2->host) return -1;
|
|
if (c1->host == c2->host) return 0;
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
msg_hash_strcmp (const void *a, const void *b)
|
|
{
|
|
return XP_STRCMP ((const char *) a, (const char *) b);
|
|
}
|
|
|
|
static uint32
|
|
HashFunc(const void* obj)
|
|
{
|
|
return XP_StringHash(((ChangeSubscribe*) obj)->groupname);
|
|
}
|
|
|
|
static int sortedptrarray_strcmp (const void* a, const void* b)
|
|
{
|
|
return XP_STRCMP (*(char **)a, *(char **)b);
|
|
}
|
|
|
|
|
|
XP_Bool imap_freehashkeys(XP_HashTable /*table*/, const void* key,
|
|
void* /*value*/, void* /*closure*/)
|
|
{
|
|
XP_FREE((char*) key);
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
MSG_SubscribePane::MSG_SubscribePane(MWContext* context, MSG_Master* master,
|
|
MSG_Host* host)
|
|
: MSG_LinedPane(context, master)
|
|
{
|
|
m_host = NULL;
|
|
XP_ASSERT(master);
|
|
if (!master) return;
|
|
msg_HostTable *newsHosts = master->GetHostTable();
|
|
|
|
m_subscribeHash = XP_HashTableNew(20, (XP_HashingFunction) HashFunc, (XP_HashCompFunction) HashComp);
|
|
if (!m_subscribeHash) return;
|
|
|
|
m_imapGroupHash = XP_HashTableNew(20, XP_StringHash, msg_hash_strcmp);
|
|
if (!m_imapGroupHash) return;
|
|
|
|
m_imapSubscribedList = new XPSortedPtrArray(sortedptrarray_strcmp);
|
|
|
|
m_imapTree = msg_IMAPGroupRecord::Create(NULL, NULL, 0, FALSE);
|
|
XP_ASSERT(m_imapTree);
|
|
if (!m_imapTree) return;
|
|
|
|
m_subscribeCommitted = FALSE;
|
|
m_expandingIMAPGroup = NULL;
|
|
|
|
m_host = host;
|
|
if (!m_host) {
|
|
if (newsHosts) m_host = newsHosts->GetDefaultHost(TRUE);
|
|
}
|
|
if (!m_host)
|
|
{
|
|
m_host = MSG_GetDefaultIMAPHost(master);
|
|
}
|
|
int numNewsHosts = newsHosts ? newsHosts->getNumHosts() : 0;
|
|
if (!m_host && numNewsHosts > 0) {
|
|
m_host = newsHosts->getHost(0);
|
|
}
|
|
m_subscribeList = NULL;
|
|
m_endSubscribeList = NULL;
|
|
|
|
// Go through the news hosts and add each group to
|
|
// the hash table
|
|
for (int i=0 ; i<numNewsHosts ; i++) {
|
|
MSG_NewsHost* h = newsHosts->getHost(i);
|
|
XP_ASSERT(h);
|
|
if (!h) continue;
|
|
MSG_FolderInfo* hostinfo = h->GetHostInfo();
|
|
XP_ASSERT(hostinfo);
|
|
if (!hostinfo) continue;
|
|
int num = hostinfo->GetNumSubFolders();
|
|
for (int j=0 ; j<num ; j++) {
|
|
MSG_FolderInfoNews* newsinfo =
|
|
(MSG_FolderInfoNews*) hostinfo->GetSubFolder(j);
|
|
XP_ASSERT(newsinfo->IsNews());
|
|
if (!newsinfo->IsNews()) continue;
|
|
if (!newsinfo->IsSubscribed()) continue;
|
|
const char* groupname = newsinfo->GetNewsgroupName();
|
|
h->NoticeNewGroup(groupname);
|
|
ChangeSubscribe* tmp = new ChangeSubscribe;
|
|
if (!tmp) break;
|
|
if (m_endSubscribeList) {
|
|
m_endSubscribeList->next = tmp;
|
|
} else {
|
|
m_subscribeList = tmp;
|
|
}
|
|
m_endSubscribeList = tmp;
|
|
tmp->host = h;
|
|
tmp->groupname = new char [XP_STRLEN(groupname) + 1];
|
|
if (!tmp->groupname) {
|
|
m_host = NULL;
|
|
return;
|
|
}
|
|
XP_STRCPY(tmp->groupname, groupname);
|
|
tmp->next = NULL;
|
|
tmp->subscribe = TRUE;
|
|
XP_Puthash(m_subscribeHash, tmp, tmp);
|
|
}
|
|
}
|
|
|
|
// We probably don't have to go through the IMAP hosts
|
|
// and add each group to the hash table, since they will
|
|
// be added on the fly
|
|
|
|
if (m_host) {
|
|
MSG_NewsHost *newsHost = m_host->GetNewsHost();
|
|
if (newsHost)
|
|
newsHost->Inhale();
|
|
SetMode(MSG_SubscribeAll, TRUE);
|
|
}
|
|
CurPane = this;
|
|
}
|
|
|
|
|
|
MSG_SubscribePane::~MSG_SubscribePane()
|
|
{
|
|
int32 i;
|
|
|
|
//if (!m_subscribeCommitted)
|
|
// CommitSubscriptions();
|
|
|
|
// Clean up the list of changed subscriptions.
|
|
// We should do this here because we don't know
|
|
// if the IMAP commits will succeed or not,
|
|
// until now.
|
|
ChangeSubscribe* tmp = m_subscribeList;
|
|
while (tmp) {
|
|
ChangeSubscribe* next = tmp->next;
|
|
delete [] tmp->groupname;
|
|
tmp->groupname = NULL;
|
|
tmp->next = NULL;
|
|
delete tmp;
|
|
tmp = next;
|
|
}
|
|
|
|
// Force all the newsrc files to be updated now. After all,
|
|
// subscribing is a major operation we don't want to lose.
|
|
msg_HostTable* hosts = GetMaster()->GetHostTable();
|
|
for (i=0 ; i < (hosts ? hosts->getNumHosts() : 0) ; i++) {
|
|
MSG_NewsHost* h = hosts->getHost(i);
|
|
XP_ASSERT(h);
|
|
if (!h) continue;
|
|
h->WriteIfDirty();
|
|
}
|
|
|
|
if (m_host && m_host->IsNews())
|
|
{
|
|
MSG_NewsHost *newsHost = m_host->GetNewsHost();
|
|
if (newsHost)
|
|
newsHost->Exhale();
|
|
}
|
|
|
|
if (m_subscribeHash)
|
|
XP_HashTableDestroy(m_subscribeHash);
|
|
|
|
ClearAndFreeIMAPGroupList();
|
|
if (m_imapGroupHash)
|
|
XP_HashTableDestroy(m_imapGroupHash);
|
|
|
|
ClearAndFreeIMAPSubscribedList();
|
|
delete m_imapSubscribedList;
|
|
|
|
for (i=0 ; i<m_groupView.GetSize() ; i++) {
|
|
delete m_groupView[i];
|
|
m_groupView[i] = NULL;
|
|
}
|
|
|
|
XP_ASSERT(CurPane == this);
|
|
if (CurPane == this) CurPane = NULL;
|
|
|
|
delete m_imapTree;
|
|
|
|
MSG_RefreshFoldersForUpdatedIMAPHosts(GetContext());
|
|
}
|
|
|
|
|
|
MSG_PaneType
|
|
MSG_SubscribePane::GetPaneType() {
|
|
return MSG_SUBSCRIBEPANE;
|
|
}
|
|
|
|
void
|
|
MSG_SubscribePane::NotifyPrefsChange(NotifyCode)
|
|
{
|
|
// ###tw Write me!
|
|
}
|
|
|
|
|
|
void
|
|
MSG_SubscribePane::ToggleExpansion(MSG_ViewIndex line, int32* numchanged)
|
|
{
|
|
DoToggleExpansion(line, numchanged);
|
|
UpdateCounts();
|
|
}
|
|
|
|
void
|
|
MSG_SubscribePane::DoToggleExpansion(MSG_ViewIndex line, int32* numchanged)
|
|
{
|
|
if (numchanged) *numchanged = 0;
|
|
XP_ASSERT(line != MSG_VIEWINDEXNONE && line < m_groupView.GetSize());
|
|
if (line == MSG_VIEWINDEXNONE || line >= m_groupView.GetSize()) return;
|
|
XP_ASSERT(m_mode == MSG_SubscribeAll);
|
|
if (m_mode != MSG_SubscribeAll) return;
|
|
msg_SubscribeLine* info = m_groupView[line];
|
|
int depth = info->GetDepth();
|
|
msg_GroupRecord* group = info->GetGroup();
|
|
XP_ASSERT(group);
|
|
if (!group) return;
|
|
if (line < m_groupView.GetSize() - 1 &&
|
|
(m_groupView[line+1]->GetDepth() > depth)) {
|
|
// OK, we are already expanded. So, collapse things, which means
|
|
// nuke all lines after the given one that have a depth bigger than
|
|
// the given line.
|
|
XP_ASSERT(group->IsExpanded());
|
|
int32 start = line + 1;
|
|
int32 end;
|
|
for (end=start ; end<m_groupView.GetSize() ; end++) {
|
|
if (m_groupView[end]->GetDepth() <= depth) break;
|
|
}
|
|
StartingUpdate(MSG_NotifyInsertOrDelete, start, - (end - start));
|
|
for (int32 i=start ; i<end ; i++) {
|
|
delete m_groupView[i];
|
|
m_groupView[i] = NULL;
|
|
}
|
|
m_groupView.RemoveAt(start, end - start);
|
|
if (numchanged) *numchanged = - (end - start);
|
|
EndingUpdate(MSG_NotifyInsertOrDelete, start, - (end - start));
|
|
group->SetIsExpanded(FALSE);
|
|
}
|
|
else
|
|
{
|
|
// We are not expanded, so expand, if possible.
|
|
if (info->CanExpand()) {
|
|
XP_ASSERT(!group->IsExpanded());
|
|
msg_SubscribeLineArray kids;
|
|
GetKidsArray(group, &kids, info->GetDepth() + 1);
|
|
XP_ASSERT(kids.GetSize() > 0); // If not, why did we think we could
|
|
// expand?
|
|
StartingUpdate(MSG_NotifyInsertOrDelete, line + 1, kids.GetSize());
|
|
m_groupView.InsertAt(line + 1, &kids);
|
|
EndingUpdate(MSG_NotifyInsertOrDelete, line + 1, kids.GetSize());
|
|
if (numchanged) *numchanged = kids.GetSize();
|
|
group->SetIsExpanded(TRUE);
|
|
}
|
|
|
|
if (m_host && m_host->IsIMAP())
|
|
{
|
|
// expanding an imap folder, search for new children
|
|
msg_IMAPGroupRecord *imapGroup = group->GetIMAPGroupRecord();
|
|
if (imapGroup && !imapGroup->GetAllGrandChildrenDiscovered())
|
|
{
|
|
m_expandingIMAPGroup = imapGroup;
|
|
MSG_IMAPHost *imapHost = m_host->GetIMAPHost();
|
|
if (imapHost)
|
|
{
|
|
char *groupname = imapGroup->GetFullName();
|
|
if (groupname)
|
|
{
|
|
char * url = CreateImapLevelChildDiscoveryUrl(imapHost->GetHostName(), groupname, imapGroup->GetHierarchySeparator(), 2);
|
|
if (url)
|
|
{
|
|
MSG_UrlQueue::AddUrlToPane(url, NULL, this);
|
|
XP_FREE(url);
|
|
}
|
|
delete groupname;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Tell the FE to redraw the expand/collapse widget of the parent group
|
|
StartingUpdate(MSG_NotifyChanged, line, 1);
|
|
EndingUpdate(MSG_NotifyChanged, line, 1);
|
|
}
|
|
|
|
|
|
void
|
|
MSG_SubscribePane::IMAPChildDiscoverySuccessful()
|
|
{
|
|
if (m_expandingIMAPGroup)
|
|
{
|
|
m_expandingIMAPGroup->SetAllGrandChildrenDiscovered(TRUE);
|
|
m_expandingIMAPGroup = NULL;
|
|
}
|
|
}
|
|
|
|
|
|
int32
|
|
MSG_SubscribePane::ExpansionDelta(MSG_ViewIndex line)
|
|
{
|
|
int32 result = 0;
|
|
XP_ASSERT(line != MSG_VIEWINDEXNONE && line < m_groupView.GetSize());
|
|
if (line == MSG_VIEWINDEXNONE || line >= m_groupView.GetSize()) return 0;
|
|
XP_ASSERT(m_mode == MSG_SubscribeAll);
|
|
if (m_mode != MSG_SubscribeAll) return 0;
|
|
msg_SubscribeLine* info = m_groupView[line];
|
|
int depth = info->GetDepth();
|
|
if (line < m_groupView.GetSize() - 1 &&
|
|
(m_groupView[line+1]->GetDepth() > depth)) {
|
|
// OK, we are already expanded. So, count how many lines after the
|
|
// given one that have a depth bigger than the given line; that's how
|
|
// many lines we would nuke.
|
|
int32 start = line + 1;
|
|
|
|
int32 end;
|
|
for (end=start ; end<m_groupView.GetSize() ; end++) {
|
|
if (m_groupView[end]->GetDepth() <= depth) break;
|
|
}
|
|
result = - (end - start);
|
|
} else {
|
|
// We are not expanded, so count how many to expand, if any.
|
|
if (info->CanExpand()) {
|
|
msg_GroupRecord* group = info->GetGroup();
|
|
XP_ASSERT(group);
|
|
if (group) {
|
|
result = GetKidsArray(group, NULL, info->GetDepth() + 1);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
int32
|
|
MSG_SubscribePane::GetNumLines()
|
|
{
|
|
return m_groupView.GetSize();
|
|
}
|
|
|
|
|
|
|
|
msg_GroupRecord*
|
|
MSG_SubscribePane::FindRealDescendent(msg_GroupRecord* group)
|
|
{
|
|
while (group && group->GetChildren()) {
|
|
group = group->GetChildren();
|
|
}
|
|
return group;
|
|
}
|
|
|
|
|
|
|
|
|
|
int32
|
|
MSG_SubscribePane::GetKidsArray(msg_GroupRecord* group,
|
|
msg_SubscribeLineArray* kids,
|
|
int depth)
|
|
{
|
|
msg_GroupRecord* child;
|
|
int32 result = 0;
|
|
int32 initsize = 0;
|
|
if (kids) initsize = kids->GetSize();
|
|
for (child = group->GetChildren(); child; child = child->GetSibling()) {
|
|
msg_SubscribeLine* info = NULL;
|
|
int32 numkids = child->GetNumKids();
|
|
if (child->IsGroup() && (child->IsIMAPGroupRecord() ?
|
|
(numkids == 0) : TRUE))
|
|
{
|
|
if (kids) {
|
|
info = new msg_SubscribeLine(child, depth,
|
|
WasSubscribed(child), 0);
|
|
kids->Add(info);
|
|
}
|
|
result++;
|
|
}
|
|
if (numkids > 0 /*&& !child->IsCategoryContainer() */ ) {
|
|
if ((numkids == 1) && !group->IsIMAPGroupRecord()) {
|
|
// If there's only one real group inside, then just go grab
|
|
// that one group, rather than wasting space by making a
|
|
// container for it.
|
|
msg_GroupRecord* tmp = FindRealDescendent(child);
|
|
if (kids) {
|
|
info = new
|
|
msg_SubscribeLine(tmp, depth,
|
|
WasSubscribed(tmp), 0);
|
|
kids->Add(info);
|
|
}
|
|
result++;
|
|
} else {
|
|
if (kids) {
|
|
info =
|
|
new msg_SubscribeLine(child, depth,
|
|
WasSubscribed(child),
|
|
child->GetNumKids());
|
|
kids->Add(info);
|
|
}
|
|
result++;
|
|
if (child->IsExpanded()) {
|
|
result += GetKidsArray(child, kids, depth + 1);
|
|
}
|
|
}
|
|
|
|
}
|
|
#ifdef NOTDEF
|
|
// I wish the below worked, but right now it turns out we can have
|
|
// some empty container groups. Sigh. ###
|
|
XP_ASSERT(kids == NULL || info != NULL); // We had to have added
|
|
// something or another...
|
|
#endif
|
|
}
|
|
XP_ASSERT(depth == 0 || result > 0); // We should never try to get any
|
|
// subnewsgroups unless there actually
|
|
// are some.
|
|
XP_ASSERT(kids == NULL || result + initsize == kids->GetSize());
|
|
return result;
|
|
}
|
|
|
|
|
|
MsgERR
|
|
MSG_SubscribePane::ToggleSubscribed(MSG_ViewIndex* indices, int32 numIndices)
|
|
{
|
|
if (!m_host) return eUNKNOWN;
|
|
for (int32 i=0 ; i<numIndices ; i++) {
|
|
MSG_ViewIndex line = indices[i];
|
|
XP_ASSERT(line != MSG_VIEWINDEXNONE && line < m_groupView.GetSize());
|
|
if (line == MSG_VIEWINDEXNONE || line >= m_groupView.GetSize()) {
|
|
continue;
|
|
}
|
|
msg_SubscribeLine* info = m_groupView[line];
|
|
msg_GroupRecord* group = info->GetGroup();
|
|
char *fullname = group->GetFullName();
|
|
if (!fullname)
|
|
{
|
|
return eOUT_OF_MEMORY;
|
|
}
|
|
ChangeSubscribe key;
|
|
key.groupname = fullname;
|
|
key.host = m_host;
|
|
ChangeSubscribe* tmp = (ChangeSubscribe*)
|
|
XP_Gethash(m_subscribeHash, &key, NULL);
|
|
if (!tmp) {
|
|
tmp = new ChangeSubscribe;
|
|
if (!tmp) return eOUT_OF_MEMORY;
|
|
if (m_endSubscribeList) {
|
|
m_endSubscribeList->next = tmp;
|
|
} else {
|
|
m_subscribeList = tmp;
|
|
}
|
|
m_endSubscribeList = tmp;
|
|
tmp->host = m_host;
|
|
tmp->groupname = fullname;
|
|
tmp->next = NULL;
|
|
XP_Puthash(m_subscribeHash, tmp, tmp);
|
|
} else {
|
|
XP_ASSERT(XP_STRCMP(tmp->groupname, fullname) == 0);
|
|
delete [] fullname;
|
|
}
|
|
XP_ASSERT(tmp->host == m_host);
|
|
tmp->subscribe = !info->GetSubscribed();
|
|
StartingUpdate(MSG_NotifyChanged, line, 1);
|
|
info->SetSubscribed(tmp->subscribe);
|
|
EndingUpdate(MSG_NotifyChanged, line, 1);
|
|
}
|
|
return eSUCCESS;
|
|
}
|
|
|
|
|
|
MsgERR
|
|
MSG_SubscribePane::SetSubscribed(MSG_ViewIndex* indices, int32 numIndices, XP_Bool subscribed)
|
|
{
|
|
if (!m_host) return eUNKNOWN;
|
|
for (int32 i=0 ; i<numIndices ; i++) {
|
|
MSG_ViewIndex line = indices[i];
|
|
XP_ASSERT(line != MSG_VIEWINDEXNONE && line < m_groupView.GetSize());
|
|
if (line == MSG_VIEWINDEXNONE || line >= m_groupView.GetSize()) {
|
|
continue;
|
|
}
|
|
msg_SubscribeLine* info = m_groupView[line];
|
|
if (info->GetSubscribed() != subscribed)
|
|
{
|
|
msg_GroupRecord* group = info->GetGroup();
|
|
char *fullname = group->GetFullName();
|
|
if (!fullname)
|
|
{
|
|
return eOUT_OF_MEMORY;
|
|
}
|
|
ChangeSubscribe key;
|
|
key.groupname = fullname;
|
|
key.host = m_host;
|
|
ChangeSubscribe* tmp = (ChangeSubscribe*)
|
|
XP_Gethash(m_subscribeHash, &key, NULL);
|
|
if (!tmp) {
|
|
tmp = new ChangeSubscribe;
|
|
if (!tmp) return eOUT_OF_MEMORY;
|
|
if (m_endSubscribeList) {
|
|
m_endSubscribeList->next = tmp;
|
|
} else {
|
|
m_subscribeList = tmp;
|
|
}
|
|
m_endSubscribeList = tmp;
|
|
tmp->host = m_host;
|
|
tmp->groupname = fullname;
|
|
tmp->next = NULL;
|
|
XP_Puthash(m_subscribeHash, tmp, tmp);
|
|
} else {
|
|
XP_ASSERT(XP_STRCMP(tmp->groupname, fullname) == 0);
|
|
delete [] fullname;
|
|
}
|
|
XP_ASSERT(tmp->host == m_host);
|
|
tmp->subscribe = subscribed;
|
|
StartingUpdate(MSG_NotifyChanged, line, 1);
|
|
info->SetSubscribed(tmp->subscribe);
|
|
EndingUpdate(MSG_NotifyChanged, line, 1);
|
|
}
|
|
}
|
|
return eSUCCESS;
|
|
}
|
|
|
|
|
|
MsgERR
|
|
MSG_SubscribePane::ExpandAll(MSG_ViewIndex* indices, int32 numIndices)
|
|
{
|
|
XP_ASSERT(m_host->IsNews());
|
|
if (!m_host->IsNews())
|
|
return 0;
|
|
|
|
// since the FE might have given us a pointer to their real selection (the
|
|
// WinFE does this), make a copy of the array so the array doesn't change
|
|
// out from under us when we call EndingUpdate
|
|
MSG_ViewIndex *tmpIndices = new MSG_ViewIndex[numIndices];
|
|
if (NULL == tmpIndices) {
|
|
return eOUT_OF_MEMORY;
|
|
}
|
|
XP_MEMCPY(tmpIndices, indices, sizeof(MSG_ViewIndex) * numIndices);
|
|
|
|
// since the FE could have constructed the list of indices in any order
|
|
// (e.g. order of discontiguous selection), we have to sort the indices so
|
|
// that we can expand the last one first (so we won't screw up any of the
|
|
// other indices.
|
|
if (numIndices > 1) {
|
|
XP_QSORT(tmpIndices, numIndices, sizeof(MSG_ViewIndex),
|
|
MSG_LinedPane::CompareViewIndices);
|
|
}
|
|
for (int32 i=numIndices-1 ; i>=0 ; i--) {
|
|
int32 n = ExpansionDelta(tmpIndices[i]);
|
|
if (n > 0) {
|
|
int32 m;
|
|
DoToggleExpansion(tmpIndices[i], &m);
|
|
XP_ASSERT(m == n);
|
|
for (MSG_ViewIndex l = tmpIndices[i] + m ; l>tmpIndices[i] ; l--) {
|
|
ExpandAll(&l, 1);
|
|
}
|
|
}
|
|
}
|
|
delete [] tmpIndices;
|
|
return 0;
|
|
}
|
|
|
|
|
|
MsgERR
|
|
MSG_SubscribePane::CollapseAll()
|
|
{
|
|
for (int32 i=m_groupView.GetSize() - 2 ; i>=0 ; i--) {
|
|
if (m_groupView[i]->GetDepth() < m_groupView[i+1]->GetDepth()) {
|
|
DoToggleExpansion(MSG_ViewIndex(i), NULL);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
MsgERR
|
|
MSG_SubscribePane::ClearNew()
|
|
{
|
|
if (!m_host) return eUNKNOWN;
|
|
if (!m_host->IsNews()) return eUNKNOWN; // only works for news
|
|
XP_ASSERT(m_mode == MSG_SubscribeNew);
|
|
MSG_NewsHost *newsHost = m_host->GetNewsHost();
|
|
if (newsHost)
|
|
newsHost->ClearNew();
|
|
else
|
|
return eUNKNOWN;
|
|
SetMode(MSG_SubscribeNew, TRUE);
|
|
return eSUCCESS;
|
|
}
|
|
|
|
|
|
|
|
void
|
|
MSG_SubscribePane::CheckForNewDone(URL_Struct* url_struct, int status,
|
|
MWContext* context)
|
|
{
|
|
if (!m_host) return;
|
|
if (!m_host->IsNews()) return;
|
|
MSG_NewsHost *newsHost = (MSG_NewsHost *)m_host;
|
|
MSG_Pane::CheckForNewDone(url_struct, status, context);
|
|
if (m_mode == MSG_SubscribeAll) {
|
|
if (status >= 0) {
|
|
newsHost->ClearNew();
|
|
newsHost->SaveHostInfo();
|
|
} else {
|
|
newsHost->Inhale(TRUE);
|
|
}
|
|
}
|
|
SetMode(m_mode, TRUE, FALSE);
|
|
if (m_callbacks.FetchCompleted) {
|
|
(*m_callbacks.FetchCompleted)(this, m_callbackclosure);
|
|
}
|
|
}
|
|
|
|
void
|
|
MSG_SubscribePane::ReportIMAPFolderDiscoveryFinished()
|
|
{
|
|
if (m_callbacks.FetchCompleted) {
|
|
(*m_callbacks.FetchCompleted)(this, m_callbackclosure);
|
|
}
|
|
}
|
|
|
|
|
|
void MSG_SubscribePane::ClearAndFreeIMAPSubscribedList()
|
|
{
|
|
char *subscribedName = NULL;
|
|
for (int j = 0; j < m_imapSubscribedList->GetSize(); j++)
|
|
{
|
|
subscribedName = (char *)(m_imapSubscribedList->GetAt(j));
|
|
XP_ASSERT (subscribedName);
|
|
delete subscribedName;
|
|
m_imapSubscribedList->RemoveAt(j);
|
|
}
|
|
}
|
|
|
|
void MSG_SubscribePane::ClearAndFreeIMAPGroupList()
|
|
{
|
|
delete m_imapTree; // deleting the root will delete all its children
|
|
XP_MapRemhash(m_imapGroupHash, imap_freehashkeys, NULL); // free the keys in the hash table
|
|
XP_Clrhash(m_imapGroupHash); // clear all the entries from the hash table
|
|
m_imapTree = msg_IMAPGroupRecord::Create(NULL, NULL, 0, FALSE); // recreate a new root
|
|
}
|
|
|
|
|
|
msg_IMAPGroupRecord *MSG_SubscribePane::FindParentRecordOfIMAPGroup(const char *folderName, char delimiter)
|
|
{
|
|
// look for this group's parent
|
|
char *parentName = XP_STRDUP(folderName);
|
|
if (!parentName)
|
|
return 0;
|
|
char *lastSlash = XP_STRRCHR(parentName, delimiter);
|
|
msg_IMAPGroupRecord *parentRecord = NULL;
|
|
if (lastSlash)
|
|
{
|
|
*lastSlash = 0;
|
|
parentRecord = (msg_IMAPGroupRecord *)(XP_Gethash(m_imapGroupHash, parentName, NULL));
|
|
}
|
|
XP_FREEIF(parentName);
|
|
return parentRecord;
|
|
}
|
|
|
|
|
|
// returns TRUE if we explicitly need to list this folder's children
|
|
// -- used only for top-level folders
|
|
// (NthLevelDiscovery is used for all other folders - easier and faster)
|
|
//
|
|
// filledInGroup is TRUE if we (the client) made up this group to act as a placeholder
|
|
// as a parent of a child which we have discovered first
|
|
XP_Bool MSG_SubscribePane::AddIMAPGroupToList(const char *folderName, char delimiter, XP_Bool isSubscribed,
|
|
uint32 folder_flags, XP_Bool filledInGroup /* = FALSE */)
|
|
{
|
|
XP_Bool needChildrenListed = FALSE;
|
|
// storage for ourFolderName is owned by the hash table m_imapGroupHash, as the key
|
|
char *ourFolderName = XP_STRDUP(folderName);
|
|
if (!ourFolderName)
|
|
return 0;
|
|
msg_IMAPGroupRecord *parentRecord = FindParentRecordOfIMAPGroup(ourFolderName, delimiter);
|
|
if (!parentRecord)
|
|
{
|
|
char *maybeParentName = XP_STRDUP(ourFolderName);
|
|
if (maybeParentName)
|
|
{
|
|
char *where = XP_STRRCHR(maybeParentName, delimiter);
|
|
if (where)
|
|
*where = 0;
|
|
if (where && *maybeParentName)
|
|
{
|
|
AddIMAPGroupToList(maybeParentName, delimiter, FALSE, 0, TRUE);
|
|
parentRecord = FindParentRecordOfIMAPGroup(ourFolderName, delimiter);
|
|
}
|
|
|
|
if (!parentRecord)
|
|
{
|
|
parentRecord = m_imapTree;
|
|
needChildrenListed = TRUE;
|
|
}
|
|
XP_FREE(maybeParentName);
|
|
}
|
|
}
|
|
|
|
if (isSubscribed)
|
|
{
|
|
// add it to the subscribe hash list - it should disallow duplicates automatically
|
|
char *subscribeFolderName = new char[XP_STRLEN(ourFolderName) + 1];
|
|
if (subscribeFolderName)
|
|
XP_STRCPY(subscribeFolderName, ourFolderName);
|
|
if (subscribeFolderName)
|
|
m_imapSubscribedList->Add(subscribeFolderName);
|
|
}
|
|
|
|
// should return TRUE if we've ever put this into the list
|
|
isSubscribed = (m_imapSubscribedList->FindIndex(0, ourFolderName) != -1);
|
|
|
|
msg_IMAPGroupRecord *lookedUpRecord = NULL;
|
|
if (!(lookedUpRecord = (msg_IMAPGroupRecord *)(XP_Gethash(m_imapGroupHash, ourFolderName, NULL))))
|
|
{
|
|
msg_IMAPGroupRecord *record = msg_IMAPGroupRecord::Create(parentRecord, ourFolderName, delimiter, filledInGroup);
|
|
if (record)
|
|
{
|
|
// set flags for the record
|
|
record->SetFlags(folder_flags);
|
|
|
|
// add to the list of known IMAP groups
|
|
XP_Puthash(m_imapGroupHash, ourFolderName, record);
|
|
|
|
// there's a parent, so add as a child if it is visible
|
|
// (or if the parent is the root)
|
|
msg_SubscribeLine *parentLine = NULL;
|
|
MSG_ViewIndex parentLineIndex = FindGroupViewIndex(parentRecord);
|
|
if (parentLineIndex != MSG_VIEWINDEXNONE)
|
|
parentLine = m_groupView[parentLineIndex];
|
|
|
|
if (parentLine || (parentRecord == m_imapTree))
|
|
{
|
|
if (parentLine)
|
|
parentLine->AddNewSubGroup();
|
|
if ((parentRecord == m_imapTree) ||
|
|
parentRecord->IsExpanded())
|
|
{
|
|
msg_IMAPGroupRecord *nextRecord = (msg_IMAPGroupRecord *)(record->GetSiblingOrAncestorSibling());
|
|
MSG_ViewIndex whereToInsert = MSG_VIEWINDEXNONE;
|
|
if (!nextRecord)
|
|
{
|
|
whereToInsert = m_groupView.GetSize();
|
|
}
|
|
else
|
|
{
|
|
whereToInsert = FindGroupViewIndex(nextRecord);
|
|
}
|
|
|
|
int16 parentDepth = parentRecord->GetDepth();
|
|
if (parentRecord == m_imapTree)
|
|
parentDepth = -1;
|
|
|
|
|
|
XP_Bool latestSubscribed = isSubscribed;
|
|
|
|
// see if we had already updated the state
|
|
// but it is uncommitted
|
|
ChangeSubscribe key;
|
|
key.groupname = ourFolderName;
|
|
key.host = m_host;
|
|
ChangeSubscribe* tmp = (ChangeSubscribe*)
|
|
XP_Gethash(m_subscribeHash, &key, NULL);
|
|
if (tmp) {
|
|
// If it was in the ChangeSubscribe list,
|
|
// then set the subscribe bit to what we had previously
|
|
// set it to.
|
|
latestSubscribed = tmp->subscribe;
|
|
}
|
|
|
|
// create the line
|
|
msg_SubscribeLine *childLine = new msg_SubscribeLine(record,
|
|
parentDepth+1, latestSubscribed, 0);
|
|
|
|
StartingUpdate(MSG_NotifyInsertOrDelete, whereToInsert, 1);
|
|
m_groupView.InsertAt(whereToInsert, childLine);
|
|
EndingUpdate(MSG_NotifyInsertOrDelete, whereToInsert, 1);
|
|
|
|
}
|
|
else
|
|
{
|
|
// make sure that there's a twisty
|
|
if (parentLineIndex != MSG_VIEWINDEXNONE)
|
|
{
|
|
StartingUpdate(MSG_NotifyChanged, parentLineIndex, 1);
|
|
EndingUpdate(MSG_NotifyChanged, parentLineIndex, 1);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// don't need to do anything, right? The parent isn't visible in
|
|
// the pane, and it's not a top-level folder (parent is root),
|
|
// so therefore the child can't be visible
|
|
}
|
|
return needChildrenListed;
|
|
}
|
|
else
|
|
return FALSE;
|
|
}
|
|
else
|
|
{
|
|
if (lookedUpRecord->GetIsGroupFilledIn())
|
|
{
|
|
// the record already existed in the hash table.
|
|
// we should update its state:
|
|
lookedUpRecord->SetIsGroupFilledIn(FALSE);
|
|
lookedUpRecord->SetHierarchySeparator(delimiter);
|
|
lookedUpRecord->SetFlags(folder_flags);
|
|
|
|
// update the line
|
|
msg_SubscribeLine *thisLine = NULL;
|
|
MSG_ViewIndex thisLineIndex = FindGroupViewIndex(lookedUpRecord);
|
|
if (thisLineIndex != MSG_VIEWINDEXNONE)
|
|
thisLine = m_groupView[thisLineIndex];
|
|
|
|
if (thisLine)
|
|
{
|
|
if (!thisLine->GetSubscribed())
|
|
thisLine->SetSubscribed(isSubscribed);
|
|
|
|
if (thisLineIndex != MSG_VIEWINDEXNONE)
|
|
{
|
|
StartingUpdate(MSG_NotifyChanged, thisLineIndex, 1);
|
|
EndingUpdate(MSG_NotifyChanged, thisLineIndex, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
FREEIF(ourFolderName);
|
|
// We might need its children
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
|
|
MsgERR
|
|
MSG_SubscribePane::FetchGroupList()
|
|
{
|
|
if (!m_host) return eUNKNOWN;
|
|
MSG_NewsHost *newsHost = m_host->GetNewsHost();
|
|
if (newsHost)
|
|
{
|
|
StartingUpdate(MSG_NotifyAll, 0, 0);
|
|
m_groupView.RemoveAll();
|
|
EndingUpdate(MSG_NotifyAll, 0, 0);
|
|
|
|
XP_ASSERT(newsHost);
|
|
|
|
newsHost->Exhale();
|
|
newsHost->EmptyInhale();
|
|
|
|
newsHost->setLastUpdate(0); // Causes us to reload everything.
|
|
|
|
return CheckForNew(newsHost);
|
|
}
|
|
else if (m_host->IsIMAP())
|
|
{
|
|
// For IMAP, this means to clear our list and perform a LIST of
|
|
// the top-level folders again.
|
|
StartingUpdate(MSG_NotifyAll, 0, 0);
|
|
// do we need to delete the storage?
|
|
m_groupView.RemoveAll();
|
|
EndingUpdate(MSG_NotifyAll, 0, 0);
|
|
|
|
ClearAndFreeIMAPGroupList();
|
|
ClearAndFreeIMAPSubscribedList();
|
|
|
|
MSG_IMAPHost *imapHost = m_host->GetIMAPHost();
|
|
if (!imapHost)
|
|
return eUNKNOWN;
|
|
char * url = CreateImapAllAndSubscribedMailboxDiscoveryUrl(imapHost->GetHostName());
|
|
if (url)
|
|
{
|
|
MSG_UrlQueue::AddUrlToPane(url, NULL, this);
|
|
XP_FREE(url);
|
|
}
|
|
|
|
return eSUCCESS;
|
|
}
|
|
else
|
|
{
|
|
// not a news or IMAP host
|
|
XP_ASSERT(0);
|
|
return eUNKNOWN;
|
|
}
|
|
}
|
|
|
|
XP_Bool
|
|
MSG_SubscribePane::AddGroupsAsNew()
|
|
{
|
|
return (m_mode != MSG_SubscribeAll);
|
|
}
|
|
|
|
|
|
|
|
MsgERR
|
|
MSG_SubscribePane::DoCommand(MSG_CommandType command, MSG_ViewIndex* indices,
|
|
int32 numIndices)
|
|
{
|
|
MsgERR status = 0;
|
|
switch (command) {
|
|
case MSG_ToggleSubscribed:
|
|
status = ToggleSubscribed(indices, numIndices);
|
|
break;
|
|
case MSG_SetSubscribed:
|
|
status = SetSubscribed(indices, numIndices, TRUE);
|
|
break;
|
|
case MSG_ClearSubscribed:
|
|
status = SetSubscribed(indices, numIndices, FALSE);
|
|
break;
|
|
case MSG_FetchGroupList:
|
|
status = FetchGroupList();
|
|
break;
|
|
case MSG_ExpandAll:
|
|
status = ExpandAll(indices, numIndices);
|
|
UpdateCounts();
|
|
break;
|
|
case MSG_CollapseAll:
|
|
status = CollapseAll();
|
|
UpdateCounts();
|
|
break;
|
|
case MSG_ClearNew:
|
|
status = ClearNew();
|
|
UpdateCounts();
|
|
break;
|
|
case MSG_CheckForNew:
|
|
if (m_host && m_host->IsNews())
|
|
{
|
|
MSG_NewsHost *newsHost = m_host->GetNewsHost();
|
|
status = newsHost ? CheckForNew(newsHost) : eUNKNOWN;
|
|
}
|
|
break;
|
|
case MSG_UpdateMessageCount:
|
|
UpdateCounts();
|
|
break;
|
|
default:
|
|
status = MSG_LinedPane::DoCommand(command, indices, numIndices);
|
|
break;
|
|
}
|
|
if (status == MK_OFFLINE)
|
|
{
|
|
FE_Alert(GetContext(), XP_GetString(XP_ALERT_OFFLINE_MODE_SELECTED));
|
|
status = 0;
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
|
|
MsgERR
|
|
MSG_SubscribePane::GetCommandStatus(MSG_CommandType command,
|
|
const MSG_ViewIndex* indices,
|
|
int32 numindices,
|
|
XP_Bool *selectable_pP,
|
|
MSG_COMMAND_CHECK_STATE *selected_pP,
|
|
const char **display_stringP,
|
|
XP_Bool *plural_pP)
|
|
{
|
|
// ###tw This never returns a display_string, right now. If someone
|
|
// actually needs it, I'll be glad to add it; but I suspect all the FE's
|
|
// are going to get these strings their own way, since these are all
|
|
// dialog box buttons and not menu items.
|
|
|
|
|
|
const char *display_string = 0;
|
|
XP_Bool plural_p = FALSE;
|
|
XP_Bool selectable_p = TRUE;
|
|
XP_Bool selected_p = FALSE;
|
|
XP_Bool selected_used_p = FALSE;
|
|
int32 i;
|
|
|
|
switch (command) {
|
|
case MSG_ToggleSubscribed:
|
|
case MSG_SetSubscribed:
|
|
case MSG_ClearSubscribed:
|
|
if (m_host && m_host->IsIMAP())
|
|
selectable_p = TRUE;
|
|
else
|
|
{
|
|
for (i=0 ; i<numindices ; i++)
|
|
{
|
|
if (!m_groupView[i]->CanExpand())
|
|
{
|
|
selectable_p = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case MSG_FetchGroupList:
|
|
selectable_p = (m_mode == MSG_SubscribeAll);
|
|
break;
|
|
case MSG_ExpandAll:
|
|
if (m_host && m_host->IsIMAP())
|
|
{
|
|
selectable_p = FALSE;
|
|
break;
|
|
}
|
|
case MSG_CollapseAll:
|
|
if (m_host)
|
|
{
|
|
if (m_host->IsNews())
|
|
{
|
|
// only allow expand all for News
|
|
for (i=0 ; i<numindices ; i++) {
|
|
if (m_groupView[i]->CanExpand()) {
|
|
selectable_p = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
selectable_p = TRUE;
|
|
}
|
|
break;
|
|
case MSG_ClearNew:
|
|
case MSG_CheckForNew:
|
|
selectable_p = (m_host && m_host->IsNews() && (m_mode == MSG_SubscribeNew));
|
|
break;
|
|
default:
|
|
return MSG_Pane::GetCommandStatus(command, indices, numindices,
|
|
selectable_pP, selected_pP,
|
|
display_stringP, plural_pP);
|
|
}
|
|
|
|
if (selectable_pP) *selectable_pP = selectable_p;
|
|
if (selected_pP) {
|
|
if (selected_used_p) {
|
|
if (selected_p) {
|
|
*selected_pP = MSG_Checked;
|
|
} else {
|
|
*selected_pP = MSG_Unchecked;
|
|
}
|
|
} else {
|
|
*selected_pP = MSG_NotUsed;
|
|
}
|
|
}
|
|
if (display_stringP) *display_stringP = display_string;
|
|
if (plural_pP) *plural_pP = plural_p;
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
int
|
|
MSG_SubscribePane::SetCallbacks(MSG_SubscribeCallbacks* callbacks,
|
|
void* closure)
|
|
{
|
|
m_callbacks = *callbacks;
|
|
m_callbackclosure = closure;
|
|
return SetMode(m_mode, TRUE);
|
|
}
|
|
|
|
|
|
int
|
|
MSG_SubscribePane::Cancel()
|
|
{
|
|
if (m_master->GetUpgradingToIMAPSubscription())
|
|
{
|
|
m_master->SetUpgradingToIMAPSubscription(FALSE);
|
|
}
|
|
|
|
ChangeSubscribe* tmp = m_subscribeList;
|
|
while (tmp) {
|
|
ChangeSubscribe* next = tmp->next;
|
|
delete [] tmp->groupname;
|
|
tmp->groupname = NULL;
|
|
tmp->next = NULL;
|
|
delete tmp;
|
|
tmp = next;
|
|
}
|
|
m_subscribeList = NULL;
|
|
XP_Clrhash(m_subscribeHash);
|
|
return 0;
|
|
}
|
|
|
|
|
|
MSG_Host*
|
|
MSG_SubscribePane::GetHost()
|
|
{
|
|
return m_host;
|
|
}
|
|
|
|
|
|
int
|
|
MSG_SubscribePane::SetHost(MSG_Host* host)
|
|
{
|
|
int status = 0;
|
|
if (host != m_host) {
|
|
if (m_host) {
|
|
MSG_NewsHost *newsHost = m_host->GetNewsHost();
|
|
if (newsHost)
|
|
status = newsHost->Exhale();
|
|
if (status < 0) return status;
|
|
}
|
|
m_host = host;
|
|
if (m_host) {
|
|
MSG_NewsHost *newsHost = m_host->GetNewsHost();
|
|
if (newsHost)
|
|
status = newsHost->Inhale();
|
|
if (status < 0) return status;
|
|
}
|
|
if (m_host && m_host->IsNews())
|
|
{
|
|
return SetMode(m_mode, TRUE);
|
|
}
|
|
else
|
|
{
|
|
// IMAP only allows subscribe, not search or new
|
|
return SetMode(MSG_SubscribeAll, TRUE);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
MSG_SubscribeMode
|
|
MSG_SubscribePane::GetMode()
|
|
{
|
|
return m_mode;
|
|
}
|
|
|
|
|
|
static int
|
|
CompareAddTimes(const void* e1, const void* e2)
|
|
{
|
|
msg_GroupRecord* g1 = (*((msg_SubscribeLine**) e1))->GetGroup();
|
|
msg_GroupRecord* g2 = (*((msg_SubscribeLine**) e2))->GetGroup();
|
|
int delta = int(int32(g2->GetAddTime()) - int32(g1->GetAddTime()));
|
|
if (delta) return delta;
|
|
|
|
XP_ASSERT(e1 != e2);
|
|
if (e1 == e2) return 0;
|
|
|
|
// Oh, boy. These two newsgroups seemed to have been added at the same
|
|
// time. We should display them in alphabetical order. But we don't
|
|
// want to just grab their fullnames, because that's pretty expensive and
|
|
// qsort compare routines should be very fast. So, we painfully compare
|
|
// their names, step by step.
|
|
// First, we find a common ancestor.
|
|
|
|
int d1 = g1->GetDepth();
|
|
int d2 = g2->GetDepth();
|
|
|
|
msg_GroupRecord* origg1 = g1;
|
|
|
|
while (d1 > d2) {
|
|
g1 = g1->GetParent();
|
|
d1--;
|
|
}
|
|
while (d2 > d1) {
|
|
g2 = g2->GetParent();
|
|
d2--;
|
|
}
|
|
if (g1 == g2) {
|
|
// This can happen only if the initial depths weren't the same, and
|
|
// one group is in fact an ancestor of the other. In that case,
|
|
// the ancestor comes first.
|
|
if (g1 == origg1) return -1;
|
|
else return 1;
|
|
}
|
|
while (g1->GetParent() != g2->GetParent()) {
|
|
g1 = g1->GetParent();
|
|
g2 = g2->GetParent();
|
|
}
|
|
return XP_STRCMP(g1->GetPartName(), g2->GetPartName());
|
|
}
|
|
|
|
|
|
int
|
|
MSG_SubscribePane::SetMode(MSG_SubscribeMode mode, XP_Bool force,
|
|
XP_Bool autofetch)
|
|
{
|
|
time_t first;
|
|
if (!force && mode == m_mode) return 0;
|
|
InterruptContext(FALSE);
|
|
StartingUpdate(MSG_NotifyAll, 0, 0);
|
|
for (int32 i=0 ; i<m_groupView.GetSize() ; i++) {
|
|
delete m_groupView[i];
|
|
m_groupView[i] = NULL;
|
|
}
|
|
m_groupView.RemoveAll();
|
|
|
|
MSG_NewsHost *newsHost = m_host ? m_host->GetNewsHost() : 0;
|
|
int32 lasttime = newsHost ? newsHost->getLastUpdate() : 0;
|
|
if (m_host) {
|
|
m_mode = mode;
|
|
switch(m_mode) {
|
|
case MSG_SubscribeAll:
|
|
if ((lasttime > 0) && newsHost) {
|
|
GetKidsArray(newsHost->GetGroupTree(), &m_groupView, 0);
|
|
}
|
|
break;
|
|
case MSG_SubscribeSearch:
|
|
XP_ASSERT(newsHost);
|
|
if (newsHost)
|
|
FindAll(m_lastSearch);
|
|
break;
|
|
case MSG_SubscribeNew:
|
|
XP_ASSERT(newsHost);
|
|
if (newsHost)
|
|
{
|
|
first = newsHost->GetFirstNewDate();
|
|
msg_GroupRecord* child;
|
|
for (child = newsHost->GetGroupTree()->GetChildren();
|
|
child;
|
|
child = child->GetNextAlphabeticNoCategories()) {
|
|
if (child->IsGroup() && child->GetAddTime() >= first) {
|
|
msg_SubscribeLine* info =
|
|
new msg_SubscribeLine(child, 0, WasSubscribed(child),
|
|
0);
|
|
if (info) m_groupView.Add(info);
|
|
}
|
|
}
|
|
m_groupView.QuickSort(CompareAddTimes);
|
|
}
|
|
break;
|
|
default:
|
|
XP_ASSERT(0);
|
|
break;
|
|
}
|
|
}
|
|
EndingUpdate(MSG_NotifyAll, 0, 0);
|
|
UpdateCounts();
|
|
if (autofetch && m_host && lasttime == 0) {
|
|
if (m_mode == MSG_SubscribeAll) {
|
|
if (m_callbacks.DoFetchGroups) {
|
|
(*m_callbacks.DoFetchGroups)(this, m_callbackclosure);
|
|
}
|
|
} else {
|
|
FE_Alert(GetContext(),
|
|
XP_GetString(MK_MSG_INCOMPLETE_NEWSGROUP_LIST));
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
MSG_ViewIndex MSG_SubscribePane::FindGroupViewIndex(msg_GroupRecord* group)
|
|
{
|
|
if (!m_host) return MSG_VIEWINDEXNONE;
|
|
XP_ASSERT(group);
|
|
if (!group) return MSG_VIEWINDEXNONE;
|
|
|
|
MSG_ViewIndex result;
|
|
for (result = 0 ; result < m_groupView.GetSize() ; result++) {
|
|
if (m_groupView[result]->GetGroup() == group) {
|
|
return result;
|
|
}
|
|
}
|
|
return MSG_VIEWINDEXNONE;
|
|
}
|
|
|
|
|
|
// only NNTP
|
|
MSG_ViewIndex
|
|
MSG_SubscribePane::FindGroupExpandIfNecessary(msg_GroupRecord* group)
|
|
{
|
|
if (!m_host) return MSG_VIEWINDEXNONE;
|
|
MSG_NewsHost *newsHost = m_host->GetNewsHost();
|
|
XP_ASSERT(newsHost);
|
|
if (!newsHost) return MSG_VIEWINDEXNONE;
|
|
XP_ASSERT(group && group != newsHost->GetGroupTree());
|
|
if (!group || group == newsHost->GetGroupTree()) return MSG_VIEWINDEXNONE;
|
|
msg_GroupRecord* parent = group->GetParent();
|
|
if (!group->IsGroup() && group->GetNumKids() == 1) {
|
|
// This group has exactly one group inside of it, and that will be
|
|
// represented by one line. So we need to be searching for that line.
|
|
// But if the search fails, then we need to expand the original parent.
|
|
group = FindRealDescendent(group);
|
|
}
|
|
|
|
MSG_ViewIndex result;
|
|
for (result = 0 ; result < m_groupView.GetSize() ; result++) {
|
|
if (m_groupView[result]->GetGroup() == group) {
|
|
return result;
|
|
}
|
|
}
|
|
// Hmm. The group must be missing because an ancestor is closed.
|
|
// Find that ancestor and expand it.
|
|
result = FindGroupExpandIfNecessary(parent);
|
|
XP_ASSERT(result != MSG_VIEWINDEXNONE);
|
|
if (result == MSG_VIEWINDEXNONE) return result;
|
|
|
|
if (ExpansionDelta(result) > 0) {
|
|
DoToggleExpansion(result, NULL);
|
|
} else if (result < m_groupView.GetSize() - 1 &&
|
|
m_groupView[result+1]->GetGroup() == parent) {
|
|
if (ExpansionDelta(result + 1) > 0) {
|
|
DoToggleExpansion(result + 1, NULL);
|
|
}
|
|
}
|
|
for (; result < m_groupView.GetSize() ; result++) {
|
|
if (m_groupView[result]->GetGroup() == group) {
|
|
return result;
|
|
}
|
|
}
|
|
return MSG_VIEWINDEXNONE;
|
|
}
|
|
|
|
|
|
// only NNTP
|
|
MSG_ViewIndex
|
|
MSG_SubscribePane::FindFirst(const char* str)
|
|
{
|
|
XP_ASSERT(str);
|
|
if (!str || !*str) return MSG_VIEWINDEXNONE;
|
|
XP_ASSERT(m_mode == MSG_SubscribeAll);
|
|
if (m_mode != MSG_SubscribeAll) return MSG_VIEWINDEXNONE;
|
|
if (!m_host) return MSG_VIEWINDEXNONE;
|
|
MSG_NewsHost *newsHost = m_host->GetNewsHost();
|
|
XP_ASSERT(newsHost);
|
|
if (!newsHost || newsHost->getLastUpdate() == 0) return MSG_VIEWINDEXNONE;
|
|
msg_GroupRecord* parent = newsHost->GetGroupTree();
|
|
MSG_ViewIndex result = MSG_VIEWINDEXNONE;
|
|
char* ptr = NULL;
|
|
|
|
while (*str) {
|
|
ptr = XP_STRCHR(str, '.');
|
|
if (ptr) *ptr = '\0';
|
|
msg_GroupRecord* child = NULL;
|
|
/*if (!parent->IsCategoryContainer()) */ {
|
|
for (child = parent->GetChildren();
|
|
child;
|
|
child = child->GetSibling()) {
|
|
if (XP_STRCMP(child->GetPartName(), str) >= 0) break;
|
|
}
|
|
}
|
|
if (!child) break;
|
|
parent = child;
|
|
if (XP_STRCMP(child->GetPartName(), str) != 0) {
|
|
// This is not an exact match. Ignore any other parts of the
|
|
// input string.
|
|
break;
|
|
}
|
|
if (ptr) {
|
|
*ptr = '.';
|
|
str = ptr + 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (ptr) *ptr = '.';
|
|
|
|
XP_ASSERT(parent);
|
|
if (!parent || parent == newsHost->GetGroupTree()) return MSG_VIEWINDEXNONE;
|
|
|
|
result = FindGroupExpandIfNecessary(parent);
|
|
UpdateCounts();
|
|
return result;
|
|
}
|
|
|
|
|
|
// only NNTP
|
|
int
|
|
MSG_SubscribePane::AddSearchResult(msg_GroupRecord* child)
|
|
{
|
|
if (!child->IsGroup()) return 0;
|
|
msg_SubscribeLine* line =
|
|
new msg_SubscribeLine(child, 0, WasSubscribed(child), 0);
|
|
if (!line) return MK_OUT_OF_MEMORY;
|
|
int size = m_groupView.GetSize();
|
|
StartingUpdate(MSG_NotifyInsertOrDelete, size, 1);
|
|
m_groupView.InsertAt(size, line);
|
|
EndingUpdate(MSG_NotifyInsertOrDelete, size, 1);
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
|
|
// only NNTP
|
|
int
|
|
MSG_SubscribePane::FindAll(const char* str)
|
|
{
|
|
XP_ASSERT(m_mode == MSG_SubscribeSearch);
|
|
if (m_mode != MSG_SubscribeSearch) return -1;
|
|
if (!m_host) return 0;
|
|
XP_ASSERT(m_host->IsNews());
|
|
MSG_NewsHost *newsHost = m_host->GetNewsHost();
|
|
if (!newsHost || newsHost->getLastUpdate() == 0) return 0;
|
|
StartingUpdate(MSG_NotifyAll, 0, 0);
|
|
m_groupView.RemoveAll();
|
|
EndingUpdate(MSG_NotifyAll, 0, 0);
|
|
|
|
if (str == NULL || *str == '\0') return 0;
|
|
|
|
msg_GroupRecord* child;
|
|
if (XP_STRCHR(str, '.') == NULL) {
|
|
// There are no dots in the search string, so we can just search the
|
|
// part names of each children.
|
|
for (child = newsHost->GetGroupTree()->GetChildren();
|
|
child ;
|
|
child = child->GetNextAlphabeticNoCategories()) {
|
|
if (XP_STRCASESTR(child->GetPartName(), str)) {
|
|
msg_GroupRecord* done = child->GetSiblingOrAncestorSibling();
|
|
for (;;) {
|
|
AddSearchResult(child);
|
|
msg_GroupRecord* next =
|
|
child->GetNextAlphabeticNoCategories();
|
|
if (next == done) break;
|
|
child = next;
|
|
}
|
|
} else {
|
|
const char* pretty = child->GetPrettyName();
|
|
if (pretty && XP_STRCASESTR(pretty, str)) {
|
|
AddSearchResult(child);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// There are dots in the search string, so we have to painfully grab
|
|
// the full name of every group to see if it matches. This can
|
|
// still be optimized, but it's harder, so I don't right now. ###tw
|
|
for (child = newsHost->GetGroupTree()->GetChildren();
|
|
child ;
|
|
child = child->GetNextAlphabeticNoCategories()) {
|
|
if (child->IsGroup()) {
|
|
char* fullname = child->GetFullName();
|
|
if (!fullname) return MK_OUT_OF_MEMORY;
|
|
if (XP_STRCASESTR(fullname, str)) {
|
|
AddSearchResult(child);
|
|
} else {
|
|
const char* pretty = child->GetPrettyName();
|
|
if (pretty && XP_STRCASESTR(pretty, str)) {
|
|
AddSearchResult(child);
|
|
}
|
|
}
|
|
delete [] fullname;
|
|
}
|
|
}
|
|
}
|
|
if (m_groupView.GetSize() == 0) {
|
|
FE_Alert(GetContext(), XP_GetString(MK_MSG_SEARCH_FAILED));
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
XP_Bool
|
|
MSG_SubscribePane::GetGroupNameLineByIndex(MSG_ViewIndex firstline,
|
|
int32 numlines,
|
|
MSG_GroupNameLine* data)
|
|
{
|
|
if (!m_host || firstline == MSG_VIEWINDEXNONE ||
|
|
firstline + numlines > m_groupView.GetSize() ||
|
|
numlines < 0) return FALSE;
|
|
int32 i;
|
|
MSG_GroupNameLine* line;
|
|
for (i=0, line=data ; i<numlines ; i++, line++) {
|
|
msg_SubscribeLine* info = m_groupView[firstline + i];
|
|
msg_GroupRecord *group = info->GetGroup();
|
|
XP_ASSERT(group);
|
|
if (!group) return FALSE;
|
|
const char *pretty = group->GetPrettyName();
|
|
if (m_host->IsNews())
|
|
{
|
|
char *tmp = group->GetFullName();
|
|
if (!tmp) return FALSE; // Out of memory.
|
|
XP_STRNCPY_SAFE(line->name, tmp, sizeof(line->name));
|
|
delete [] tmp;
|
|
}
|
|
else
|
|
{
|
|
// IMAP - don't display the whole path
|
|
const char *partname = group->GetPartName();
|
|
if (!partname) return FALSE;
|
|
XP_STRNCPY_SAFE(line->name, partname, sizeof(line->name));
|
|
}
|
|
line->level = info->GetDepth();
|
|
line->total = info->GetNumMessages();
|
|
line->flags = 0;
|
|
MSG_NewsHost *newsHost = m_host->GetNewsHost();
|
|
if (newsHost && group->GetAddTime() > newsHost->GetFirstNewDate()) {
|
|
line->flags |= MSG_GROUPNAME_FLAG_NEW_GROUP;
|
|
}
|
|
if (info->CanExpand()) {
|
|
int length = XP_STRLEN(line->name);
|
|
if (m_host->IsNews())
|
|
{
|
|
PR_snprintf(line->name + length, sizeof(line->name) - length - 1,
|
|
".* (%ld groups)", info->GetNumSubGroups());
|
|
// ###tw I18N the above string. (Of course, it wasn't i18n in
|
|
// 3.0, either.)
|
|
}
|
|
else
|
|
{
|
|
int32 numSubGroups = info->GetNumSubGroups();
|
|
char *subfolderString = numSubGroups == 1 ?
|
|
"subfolder" : "subfolders";
|
|
PR_snprintf(line->name + length, sizeof(line->name) - length - 1,
|
|
".* (%ld %s)", numSubGroups, subfolderString);
|
|
// ###tw I18N the above string. (Of course, it wasn't i18n in
|
|
// 3.0, either.)
|
|
}
|
|
line->flags |= MSG_GROUPNAME_FLAG_HASCHILDREN;
|
|
if (firstline + i >= m_groupView.GetSize() - 1 ||
|
|
m_groupView[firstline + i + 1]->GetDepth() <= line->level) {
|
|
line->flags |= MSG_GROUPNAME_FLAG_ELIDED;
|
|
}
|
|
} else if (newsHost && pretty) {
|
|
int length = XP_STRLEN(line->name);
|
|
PR_snprintf(line->name + length, sizeof(line->name) - length - 1,
|
|
" (%s)", pretty);
|
|
}
|
|
if (info->GetSubscribed()) {
|
|
line->flags |= MSG_GROUPNAME_FLAG_SUBSCRIBED;
|
|
}
|
|
|
|
// set the folder type flags (IMAP public, IMAP personal, etc.)
|
|
msg_IMAPGroupRecord *imapGroup = group->GetIMAPGroupRecord();
|
|
if (imapGroup)
|
|
line->flags |= imapGroup->GetFlags();
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
|
|
XP_Bool
|
|
MSG_SubscribePane::WasSubscribed(msg_GroupRecord* group)
|
|
{
|
|
char* groupname = group->GetFullName();
|
|
if (!groupname || !m_host) return FALSE;
|
|
|
|
// first look through the updated (uncommitted) subscribe list
|
|
ChangeSubscribe key;
|
|
key.groupname = groupname;
|
|
key.host = m_host;
|
|
ChangeSubscribe* tmp = (ChangeSubscribe*)
|
|
XP_Gethash(m_subscribeHash, &key, NULL);
|
|
if (tmp) {
|
|
delete [] groupname;
|
|
return tmp->subscribe;
|
|
}
|
|
|
|
// next, if it's IMAP, look through the original subscribed list
|
|
// that we first got back from the server
|
|
if (group->IsIMAPGroupRecord())
|
|
{
|
|
XP_Bool wasSubscribed = (m_imapSubscribedList->FindIndex(0, groupname) != -1);
|
|
delete [] groupname;
|
|
return wasSubscribed;
|
|
}
|
|
|
|
// otherwise, we must not be subscribed
|
|
delete [] groupname;
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
void
|
|
MSG_SubscribePane::UpdateCounts()
|
|
{
|
|
if (m_host && !NET_IsOffline())
|
|
{
|
|
MSG_NewsHost *newsHost = m_host->GetNewsHost();
|
|
if (newsHost)
|
|
{
|
|
for (int i=0 ; i<m_groupView.GetSize() ; i++) {
|
|
msg_SubscribeLine* info = m_groupView[i];
|
|
if (info->GetNumMessages() < 0 && info->GetNumSubGroups() == 0) {
|
|
// OK, found one, so kick off the URL.
|
|
InterruptContext(FALSE);
|
|
UpdateNewsCounts(newsHost);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void
|
|
MSG_SubscribePane::UpdateNewsCountsDone(int status)
|
|
{
|
|
if (status >= 0) {
|
|
UpdateCounts();
|
|
}
|
|
}
|
|
|
|
|
|
int32
|
|
MSG_SubscribePane::GetNewsRCCount(MSG_NewsHost* host)
|
|
{
|
|
XP_ASSERT(host == m_host);
|
|
if (host != m_host) return 0;
|
|
int num = m_groupView.GetSize();
|
|
int32 result = 0;
|
|
for (int i=0 ; i<num ; i++) {
|
|
msg_SubscribeLine* info = m_groupView[i];
|
|
if (info->GetNumMessages() < 0 && info->GetNumSubGroups() == 0) {
|
|
result++;
|
|
}
|
|
}
|
|
m_curUpdate = 0;
|
|
return result;
|
|
}
|
|
|
|
|
|
char*
|
|
MSG_SubscribePane::GetNewsRCGroup(MSG_NewsHost* host)
|
|
{
|
|
XP_ASSERT(host == m_host);
|
|
if (host != m_host) return NULL;
|
|
int num = m_groupView.GetSize();
|
|
if (num <= 0) return NULL;
|
|
if (m_curUpdate >= num) m_curUpdate = 0;
|
|
int32 i = m_curUpdate;
|
|
do {
|
|
msg_SubscribeLine* info = m_groupView[i];
|
|
if (info->GetNumMessages() < 0 && info->GetNumSubGroups() == 0) {
|
|
info->SetNumMessages(0); // so we don't keep returning this one.
|
|
msg_GroupRecord* group = info->GetGroup();
|
|
XP_ASSERT(group);
|
|
if (!group) continue;
|
|
char* tmp = group->GetFullName();
|
|
char* result = NULL;
|
|
if (tmp) {
|
|
result = XP_STRDUP(tmp); // ### Ick. malloc vs. new.
|
|
delete [] tmp;
|
|
}
|
|
m_curUpdate = i;
|
|
return result;
|
|
}
|
|
i++;
|
|
if (i >= num) i = 0;
|
|
} while (i != m_curUpdate);
|
|
return NULL;
|
|
}
|
|
|
|
|
|
|
|
int
|
|
MSG_SubscribePane::DisplaySubscribedGroup(MSG_NewsHost* host,
|
|
const char *groupname,
|
|
int32 /*oldest_message*/,
|
|
int32 /*youngest_message*/,
|
|
int32 total_messages,
|
|
XP_Bool /*nowvisiting*/)
|
|
{
|
|
XP_ASSERT(host == m_host);
|
|
if (host != m_host) return 0;
|
|
|
|
XP_ASSERT(m_curUpdate >= 0 && m_curUpdate < m_groupView.GetSize());
|
|
if (m_curUpdate < 0 || m_curUpdate >= m_groupView.GetSize()) return -1;
|
|
|
|
msg_SubscribeLine* info = m_groupView[m_curUpdate];
|
|
msg_GroupRecord* group = info->GetGroup();
|
|
XP_ASSERT(group);
|
|
if (!group) return -1;
|
|
char* tmp = group->GetFullName();
|
|
XP_Bool matches = (XP_STRCMP(groupname, tmp) == 0);
|
|
delete [] tmp;
|
|
if (matches) {
|
|
StartingUpdate(MSG_NotifyChanged, m_curUpdate, 1);
|
|
info->SetNumMessages(total_messages);
|
|
EndingUpdate(MSG_NotifyChanged, m_curUpdate, 1);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
int MSG_SubscribePane::CommitSubscriptions()
|
|
{
|
|
DisableUpdate();
|
|
InterruptContext(FALSE);
|
|
ChangeSubscribe* tmp = m_subscribeList;
|
|
XP_Bool runningIMAP = FALSE;
|
|
while (tmp) {
|
|
XP_ASSERT(tmp->groupname);
|
|
if (tmp->groupname)
|
|
{
|
|
if (tmp->host->IsNews())
|
|
{
|
|
MSG_NewsHost *newsHost = tmp->host->GetNewsHost();
|
|
if (tmp->subscribe) {
|
|
newsHost->AddGroup(tmp->groupname);
|
|
} else {
|
|
newsHost->RemoveGroup(tmp->groupname);
|
|
}
|
|
//delete [] tmp->groupname;
|
|
//tmp->groupname = NULL;
|
|
}
|
|
else if (tmp->host->IsIMAP())
|
|
{
|
|
MSG_IMAPHost *imapHost = tmp->host->GetIMAPHost();
|
|
// here's where we have to issue a subscribe or unsubscribe to the server
|
|
|
|
if (imapHost)
|
|
{
|
|
char *imapSubscribeURL = 0;
|
|
// whether or not we were originally subscribed to this folder
|
|
XP_Bool originallySubscribed = (m_imapSubscribedList->FindIndex(0, tmp->groupname) != -1);
|
|
|
|
if (tmp->subscribe && !originallySubscribed)
|
|
{
|
|
imapSubscribeURL = CreateIMAPSubscribeMailboxURL(imapHost->GetHostName(), tmp->groupname);
|
|
if (imapHost->GetIsHostUsingSubscription() || m_master->GetUpgradingToIMAPSubscription())
|
|
imapHost->SetHostNeedsFolderUpdate(TRUE); // we only need to update the folder view if we're only
|
|
// showing subscribed folders
|
|
}
|
|
else if (!tmp->subscribe && originallySubscribed)
|
|
{
|
|
imapSubscribeURL = CreateIMAPUnsubscribeMailboxURL(imapHost->GetHostName(), tmp->groupname);
|
|
if (imapHost->GetIsHostUsingSubscription() || m_master->GetUpgradingToIMAPSubscription())
|
|
imapHost->SetHostNeedsFolderUpdate(TRUE); // we only need to update the folder view if we're only
|
|
// showing subscribed folders
|
|
}
|
|
|
|
if (imapSubscribeURL)
|
|
{
|
|
runningIMAP = TRUE;
|
|
MSG_UrlQueue::AddUrlToPane(imapSubscribeURL, NULL, this);
|
|
XP_FREE(imapSubscribeURL);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// not News or IMAP - what is it?
|
|
XP_ASSERT(0);
|
|
}
|
|
}
|
|
ChangeSubscribe* next = tmp->next;
|
|
//delete tmp;
|
|
tmp = next;
|
|
}
|
|
|
|
if (m_master->GetUpgradingToIMAPSubscription())
|
|
{
|
|
m_master->SetIMAPSubscriptionUpgradedPrefs();
|
|
m_master->SetUpgradingToIMAPSubscription(FALSE);
|
|
}
|
|
|
|
if (!runningIMAP)
|
|
{
|
|
// make sure the FE knows it's ok to shut down the pane
|
|
FE_AllConnectionsComplete(GetContext());
|
|
}
|
|
|
|
return 0;
|
|
}
|