/* kopetecontact.cpp - Kopete Contact Copyright (c) 2002-2004 by Duncan Mac-Vicar Prett Copyright (c) 2002-2003 by Martijn Klingens Copyright (c) 2002-2004 by Olivier Goffart Kopete (c) 2002-2004 by the Kopete developers ************************************************************************* * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Lesser General Public * * License as published by the Free Software Foundation; either * * version 2 of the License, or (at your option) any later version. * * * ************************************************************************* */ #include "kopetecontact.h" #include #include #include #include #include #include #include #include #include #include #include "kopetecontactlist.h" #include "kopeteglobal.h" #include "kopeteuiglobal.h" #include "kopeteprotocol.h" #include "kopeteaccount.h" #include "kopetestdaction.h" #include "kopetechatsession.h" #include "kopeteview.h" #include "kopetemetacontact.h" #include "kopeteprefs.h" #include "metacontactselectorwidget.h" #include "kopeteemoticons.h" //For the moving to another metacontact dialog #include #include #include #include #include #include #include #include namespace Kopete { struct Contact::Private { public: bool fileCapable; OnlineStatus onlineStatus; Account *account; MetaContact *metaContact; TQString contactId; TQString icon; TQTime idleTimer; unsigned long int idleTime; Kopete::ContactProperty::Map properties; }; Contact::Contact( Account *account, const TQString &contactId, MetaContact *parent, const TQString &icon ) : TQObject( parent ) { d = new Private; //kdDebug( 14010 ) << k_funcinfo << "Creating contact with id " << contactId << endl; d->contactId = contactId; d->metaContact = parent; d->fileCapable = false; d->account = account; d->idleTime = 0; d->icon = icon; // If can happend that a MetaContact may be used without a account // (ex: for unit tests or chat window style preview) if ( account ) { account->registerContact( this ); connect( account, TQT_SIGNAL( isConnectedChanged() ), TQT_SLOT( slotAccountIsConnectedChanged() ) ); } // Need to check this because myself() may have no parent // Maybe too the metaContact doesn't have a valid protocol() // (ex: for unit tests or chat window style preview) if( parent && protocol() ) { connect( parent, TQT_SIGNAL( aboutToSave( Kopete::MetaContact * ) ), protocol(), TQT_SLOT( slotMetaContactAboutToSave( Kopete::MetaContact * ) ) ); parent->addContact( this ); } } Contact::~Contact() { //kdDebug(14010) << k_funcinfo << endl; emit( contactDestroyed( this ) ); delete d; } OnlineStatus Contact::onlineStatus() const { if ( this == account()->myself() || account()->isConnected() ) return d->onlineStatus; else return protocol()->accountOfflineStatus(); } void Contact::setOnlineStatus( const OnlineStatus &status ) { if( status == d->onlineStatus ) return; OnlineStatus oldStatus = d->onlineStatus; d->onlineStatus = status; Kopete::Global::Properties *globalProps = Kopete::Global::Properties::self(); // Contact changed from Offline to another status if( oldStatus.status() == OnlineStatus::Offline && status.status() != OnlineStatus::Offline ) { setProperty( globalProps->onlineSince(), TQDateTime::currentDateTime() ); /*kdDebug(14010) << k_funcinfo << "REMOVING lastSeen property for " << d->displayName << endl;*/ removeProperty( globalProps->lastSeen() ); } else if( oldStatus.status() != OnlineStatus::Offline && oldStatus.status() != OnlineStatus::Unknown && status.status() == OnlineStatus::Offline ) // Contact went back offline { removeProperty( globalProps->onlineSince() ); /*kdDebug(14010) << k_funcinfo << "SETTING lastSeen property for " << d->displayName << endl;*/ setProperty( globalProps->lastSeen(), TQDateTime::currentDateTime() ); } if ( this == account()->myself() || account()->isConnected() ) emit onlineStatusChanged( this, status, oldStatus ); } void Contact::slotAccountIsConnectedChanged() { if ( this == account()->myself() ) return; if ( account()->isConnected() ) emit onlineStatusChanged( this, d->onlineStatus, protocol()->accountOfflineStatus() ); else emit onlineStatusChanged( this, protocol()->accountOfflineStatus(), d->onlineStatus ); } void Contact::sendFile( const KURL &, const TQString &, uint ) { kdWarning( 14010 ) << k_funcinfo << "Plugin " << protocol()->pluginId() << " has enabled file sending, " << "but didn't implement it!" << endl; } void Contact::slotAddContact() { if( metaContact() ) { metaContact()->setTemporary( false ); ContactList::self()->addMetaContact( metaContact() ); } } KPopupMenu* Contact::popupMenu( ChatSession *manager ) { // Build the menu KPopupMenu *menu = new KPopupMenu(); // insert title TQString titleText; TQString nick = property( Kopete::Global::Properties::self()->nickName() ).value().toString(); if( nick.isEmpty() ) titleText = TQString::fromLatin1( "%1 (%2)" ).arg( contactId(), onlineStatus().description() ); else titleText = TQString::fromLatin1( "%1 <%2> (%3)" ).arg( nick, contactId(), onlineStatus().description() ); menu->insertTitle( titleText ); if( metaContact() && metaContact()->isTemporary() && contactId() != account()->myself()->contactId() ) { KAction *actionAddContact = new KAction( i18n( "&Add to Your Contact List" ), TQString::fromLatin1( "add_user" ), 0, this, TQT_SLOT( slotAddContact() ), menu, "actionAddContact" ); actionAddContact->plug( menu ); menu->insertSeparator(); } // FIXME: After KDE 3.2 we should make isReachable do the isConnected call so it can be removed here - Martijn bool reach = account()->isConnected() && isReachable(); bool myself = (this == account()->myself()); KAction *actionSendMessage = KopeteStdAction::sendMessage( this, TQT_SLOT( sendMessage() ), menu, "actionSendMessage" ); actionSendMessage->setEnabled( reach && !myself ); actionSendMessage->plug( menu ); KAction *actionChat = KopeteStdAction::chat( this, TQT_SLOT( startChat() ), menu, "actionChat" ); actionChat->setEnabled( reach && !myself ); actionChat->plug( menu ); KAction *actionSendFile = KopeteStdAction::sendFile( this, TQT_SLOT( sendFile() ), menu, "actionSendFile" ); actionSendFile->setEnabled( reach && d->fileCapable && !myself ); actionSendFile->plug( menu ); // Protocol specific options will go below this separator // through the use of the customContextMenuActions() function // Get the custom actions from the protocols ( pure virtual function ) TQPtrList *customActions = customContextMenuActions( manager ); if( customActions && !customActions->isEmpty() ) { menu->insertSeparator(); for( KAction *a = customActions->first(); a; a = customActions->next() ) a->plug( menu ); } delete customActions; menu->insertSeparator(); if( metaContact() && !metaContact()->isTemporary() ) KopeteStdAction::changeMetaContact( this, TQT_SLOT( changeMetaContact() ), menu, "actionChangeMetaContact" )->plug( menu ); KopeteStdAction::contactInfo( this, TQT_SLOT( slotUserInfo() ), menu, "actionUserInfo" )->plug( menu ); #if 0 //this is not fully implemented yet (and doesn't work). disable for now - Olivier 2005-01-11 if ( account()->isBlocked( d->contactId ) ) KopeteStdAction::unblockContact( this, TQT_SLOT( slotUnblock() ), menu, "actionUnblockContact" )->plug( menu ); else KopeteStdAction::blockContact( this, TQT_SLOT( slotBlock() ), menu, "actionBlockContact" )->plug( menu ); #endif if( metaContact() && !metaContact()->isTemporary() ) KopeteStdAction::deleteContact( this, TQT_SLOT( slotDelete() ), menu, "actionDeleteContact" )->plug( menu ); return menu; } void Contact::changeMetaContact() { KDialogBase *moveDialog = new KDialogBase( Kopete::UI::Global::mainWidget(), "moveDialog", true, i18n( "Move Contact" ), KDialogBase::Ok | KDialogBase::Cancel, KDialogBase::Ok, true ); TQVBox *w = new TQVBox( moveDialog ); w->setSpacing( KDialog::spacingHint() ); Kopete::UI::MetaContactSelectorWidget *selector = new Kopete::UI::MetaContactSelectorWidget(w); selector->setLabelMessage(i18n( "Select the meta contact to which you want to move this contact:" )); // exclude this metacontact as a target metacontact for the move selector->excludeMetaContact( metaContact() ); TQCheckBox *chkCreateNew = new TQCheckBox( i18n( "Create a new metacontact for this contact" ), w ); TQWhatsThis::add( chkCreateNew , i18n( "If you select this option, a new metacontact will be created in the top-level group " "with the name of this contact and the contact will be moved to it." ) ); TQObject::connect( chkCreateNew , TQT_SIGNAL( toggled(bool) ) , selector , TQT_SLOT ( setDisabled(bool) ) ) ; moveDialog->setMainWidget(w); if( moveDialog->exec() == TQDialog::Accepted ) { Kopete::MetaContact *mc = selector->metaContact(); if(chkCreateNew->isChecked()) { mc=new Kopete::MetaContact(); Kopete::ContactList::self()->addMetaContact(mc); } if( mc ) { setMetaContact( mc ); } } moveDialog->deleteLater(); } void Contact::setMetaContact( MetaContact *m ) { MetaContact *old = d->metaContact; if(old==m) //that make no sens return; if( old ) { int result=KMessageBox::No; if( old->isTemporary() ) result=KMessageBox::Yes; else if( old->contacts().count()==1 ) { //only one contact, including this one, that mean the contact will be empty efter the move result = KMessageBox::questionYesNoCancel( Kopete::UI::Global::mainWidget(), i18n( "You are moving the contact `%1' to the meta contact `%2'.\n" "`%3' will be empty afterwards. Do you want to delete this contact?" ) .arg(contactId(), m ? m->displayName() : TQString::null, old->displayName()) , i18n( "Move Contact" ), KStdGuiItem::del(), i18n( "&Keep" ) , TQString::fromLatin1("delete_old_contact_when_move") ); if(result==KMessageBox::Cancel) return; } old->removeContact( this ); disconnect( old, TQT_SIGNAL( aboutToSave( Kopete::MetaContact * ) ), protocol(), TQT_SLOT( slotMetaContactAboutToSave( Kopete::MetaContact * ) ) ); if(result==KMessageBox::Yes) { //remove the old metacontact. (this delete the MC) ContactList::self()->removeMetaContact(old); } else { d->metaContact = m; //i am forced to do that now if i want the next line works //remove cached data for this protocol which will not be removed since we disconnected protocol()->slotMetaContactAboutToSave( old ); } } d->metaContact = m; if( m ) { m->addContact( this ); m->insertChild( this ); // it is necessary to call this write here, because MetaContact::addContact() does not differentiate // between adding completely new contacts (which should be written to kabc) and restoring upon restart // (where no write is needed). KABCPersistence::self()->write( m ); connect( d->metaContact, TQT_SIGNAL( aboutToSave( Kopete::MetaContact * ) ), protocol(), TQT_SLOT( slotMetaContactAboutToSave( Kopete::MetaContact * ) ) ); } sync(); } void Contact::serialize( TQMap &/*serializedData*/, TQMap & /* addressBookData */ ) { } void Contact::serializeProperties(TQMap &serializedData) { Kopete::ContactProperty::Map::ConstIterator it;// = d->properties.ConstIterator; for (it=d->properties.begin(); it != d->properties.end(); ++it) { if (!it.data().tmpl().persistent()) continue; TQVariant val = it.data().value(); TQString key = TQString::fromLatin1("prop_%1_%2").arg(TQString::fromLatin1(val.typeName()), it.key()); serializedData[key] = val.toString(); } // end for() } // end serializeProperties() void Contact::deserializeProperties( TQMap &serializedData ) { TQMap::ConstIterator it; for ( it=serializedData.begin(); it != serializedData.end(); ++it ) { TQString key = it.key(); if ( !key.startsWith( TQString::fromLatin1("prop_") ) ) // avoid parsing other serialized data continue; TQStringList keyList = TQStringList::split( TQChar('_'), key, false ); if( keyList.count() < 3 ) // invalid key, not enough parts in string "prop_X_Y" continue; key = keyList[2]; // overwrite key var with the real key name this property has TQString type( keyList[1] ); // needed for TQVariant casting TQVariant variant( it.data() ); if( !variant.cast(TQVariant::nameToType(type.latin1())) ) { kdDebug(14010) << k_funcinfo << "Casting TQVariant to needed type FAILED" << "key=" << key << ", type=" << type << endl; continue; } Kopete::ContactPropertyTmpl tmpl = Kopete::Global::Properties::self()->tmpl(key); if( tmpl.isNull() ) { kdDebug( 14010 ) << k_funcinfo << "no ContactPropertyTmpl defined for" \ " key " << key << ", cannot restore persistent property" << endl; continue; } setProperty(tmpl, variant); } // end for() } bool Contact::isReachable() { // The default implementation returns false when offline and true // otherwise. Subclass if you need more control over the process. return onlineStatus().status() != OnlineStatus::Offline; } void Contact::startChat() { KopeteView *v=manager( CanCreate )->view(true, TQString::fromLatin1("kopete_chatwindow") ); if(v) v->raise(true); } void Contact::sendMessage() { KopeteView *v=manager( CanCreate )->view(true, TQString::fromLatin1("kopete_emailwindow") ); if(v) v->raise(true); } void Contact::execute() { // FIXME: After KDE 3.2 remove the isConnected check and move it to isReachable - Martijn if ( account()->isConnected() && isReachable() ) { KopeteView *v=manager( CanCreate )->view(true, KopetePrefs::prefs()->interfacePreference() ); if(v) v->raise(true); } else { KMessageBox::queuedMessageBox( Kopete::UI::Global::mainWidget(), KMessageBox::Sorry, i18n( "This user is not reachable at the moment. Please try a protocol that supports offline sending, or wait " "until this user comes online." ), i18n( "User is Not Reachable" ) ); } } void Contact::slotDelete() { if ( KMessageBox::warningContinueCancel( Kopete::UI::Global::mainWidget(), i18n( "Are you sure you want to remove the contact '%1' from your contact list?" ). arg( d->contactId ), i18n( "Remove Contact" ), KGuiItem(i18n("Remove"), TQString::fromLatin1("delete_user") ), TQString::fromLatin1("askRemoveContact"), KMessageBox::Notify | KMessageBox::Dangerous ) == KMessageBox::Continue ) { deleteContact(); } } void Contact::deleteContact() { // Default implementation simply deletes the contact deleteLater(); } MetaContact * Contact::metaContact() const { return d->metaContact; } TQString Contact::contactId() const { return d->contactId; } Protocol * Contact::protocol() const { return d->account ? d->account->protocol() : 0L; } Account * Contact::account() const { return d->account; } void Contact::sync(unsigned int) { /* Default implementation does nothing */ } TQString& Contact::icon() const { return d->icon; } void Contact::setIcon( const TQString& icon ) { d->icon = icon; return; } TQPtrList *Contact::customContextMenuActions() { return 0L; } TQPtrList *Contact::customContextMenuActions( ChatSession * /* manager */ ) { return customContextMenuActions(); } bool Contact::isOnline() const { return onlineStatus().isDefinitelyOnline(); } bool Contact::isFileCapable() const { return d->fileCapable; } void Contact::setFileCapable( bool filecap ) { d->fileCapable = filecap; } bool Contact::canAcceptFiles() const { return isOnline() && d->fileCapable; } unsigned long int Contact::idleTime() const { if(d->idleTime==0) return 0; return d->idleTime+(d->idleTimer.elapsed()/1000); } void Contact::setIdleTime( unsigned long int t ) { bool idleChanged = false; if(d->idleTime != t) idleChanged = true; d->idleTime=t; if(t > 0) d->idleTimer.start(); //FIXME: if t == 0, idleTime() will now return garbage // else // d->idleTimer.stop(); if(idleChanged) emit idleStateChanged(this); } TQStringList Contact::properties() const { return d->properties.keys(); } bool Contact::hasProperty(const TQString &key) const { return d->properties.contains(key); } const ContactProperty &Contact::property(const TQString &key) const { if(hasProperty(key)) return d->properties[key]; else return Kopete::ContactProperty::null; } const Kopete::ContactProperty &Contact::property( const Kopete::ContactPropertyTmpl &tmpl) const { if(hasProperty(tmpl.key())) return d->properties[tmpl.key()]; else return Kopete::ContactProperty::null; } void Contact::setProperty(const Kopete::ContactPropertyTmpl &tmpl, const TQVariant &value) { if(tmpl.isNull() || tmpl.key().isEmpty()) { kdDebug(14000) << k_funcinfo << "No valid template for property passed!" << endl; return; } if(value.isNull() || value.canCast(TQVariant::String) && value.toString().isEmpty()) { removeProperty(tmpl); } else { TQVariant oldValue = property(tmpl.key()).value(); if(oldValue != value) { Kopete::ContactProperty prop(tmpl, value); d->properties.insert(tmpl.key(), prop, true); emit propertyChanged(this, tmpl.key(), oldValue, value); } } } void Contact::removeProperty(const Kopete::ContactPropertyTmpl &tmpl) { if(!tmpl.isNull() && !tmpl.key().isEmpty()) { TQVariant oldValue = property(tmpl.key()).value(); d->properties.remove(tmpl.key()); emit propertyChanged(this, tmpl.key(), oldValue, TQVariant()); } } TQString Contact::toolTip() const { Kopete::ContactProperty p; TQString tip; TQStringList shownProps = KopetePrefs::prefs()->toolTipContents(); // -------------------------------------------------------------------------- // Fixed part of tooltip TQString iconName = TQString::fromLatin1("kopete-contact-icon:%1:%2:%3") .arg( KURL::encode_string( protocol()->pluginId() ), KURL::encode_string( account()->accountId() ), KURL::encode_string( contactId() ) ); // TODO: the nickname should be a configurable properties, like others. -Olivier TQString nick = property( Kopete::Global::Properties::self()->nickName() ).value().toString(); if ( nick.isEmpty() ) { tip = i18n( "DISPLAY NAME
 CONTACT STATUS", "%3
 %1" ). arg( Kopete::Message::escape( onlineStatus().description() ), iconName, Kopete::Message::escape( d->contactId ) ); } else { tip = i18n( "DISPLAY NAME (CONTACT ID)
 CONTACT STATUS", "%4 (%3)
 %1" ). arg( Kopete::Message::escape( onlineStatus().description() ), iconName, Kopete::Message::escape( contactId() ), Kopete::Emoticons::parseEmoticons( Kopete::Message::escape( nick ) ) ); } // -------------------------------------------------------------------------- // Configurable part of tooltip for(TQStringList::Iterator it=shownProps.begin(); it!=shownProps.end(); ++it) { if((*it) == TQString::fromLatin1("FormattedName")) { TQString name = formattedName(); if(!name.isEmpty()) { tip += i18n("
Full Name: FORMATTED NAME", "
Full Name: %1").arg(TQStyleSheet::escape(name)); } } else if ((*it) == TQString::fromLatin1("FormattedIdleTime")) { TQString time = formattedIdleTime(); if(!time.isEmpty()) { tip += i18n("
Idle: FORMATTED IDLE TIME", "
Idle: %1").arg(time); } } else if ((*it) == TQString::fromLatin1("homePage")) { TQString url = property(*it).value().toString(); if(!url.isEmpty()) { tip += i18n("
Home Page: FORMATTED URL", "
Home Page: %2"). arg( KURL::encode_string( url ), Kopete::Message::escape( TQStyleSheet::escape(url) ) ); } } else if ((*it) == TQString::fromLatin1("awayMessage")) { TQString awaymsg = property(*it).value().toString(); if(!awaymsg.isEmpty()) { tip += i18n("
Away Message: FORMATTED AWAY MESSAGE", "
Away Message: %1").arg ( Kopete::Emoticons::parseEmoticons( Kopete::Message::escape(awaymsg) ) ); } } else { p = property(*it); if(!p.isNull()) { TQVariant val = p.value(); TQString valueText; switch(val.type()) { case TQVariant::DateTime: valueText = KGlobal::locale()->formatDateTime(val.toDateTime()); valueText = Kopete::Message::escape( valueText ); break; case TQVariant::Date: valueText = KGlobal::locale()->formatDate(val.toDate()); valueText = Kopete::Message::escape( valueText ); break; case TQVariant::Time: valueText = KGlobal::locale()->formatTime(val.toTime()); valueText = Kopete::Message::escape( valueText ); break; default: if( p.isRichText() ) { valueText = val.toString(); } else { valueText = Kopete::Message::escape( val.toString() ); } } tip += i18n("
PROPERTY LABEL: PROPERTY VALUE", "
%2: %1"). arg( valueText, TQStyleSheet::escape(p.tmpl().label()) ); } } } return tip; } TQString Kopete::Contact::formattedName() const { if( hasProperty(TQString::fromLatin1("FormattedName")) ) return property(TQString::fromLatin1("FormattedName")).value().toString(); TQString ret; Kopete::ContactProperty first, last; first = property(TQString::fromLatin1("firstName")); last = property(TQString::fromLatin1("lastName")); if(!first.isNull()) { if(!last.isNull()) // contact has both first and last name { ret = i18n("firstName lastName", "%2 %1") .arg(last.value().toString()) .arg(first.value().toString()); } else // only first name set { ret = first.value().toString(); } } else if(!last.isNull()) // only last name set { ret = last.value().toString(); } return ret; } TQString Kopete::Contact::formattedIdleTime() const { TQString ret; unsigned long int leftTime = idleTime(); if ( leftTime > 0 ) { // FIXME: duplicated from code in kopetecontactlistview.cpp unsigned long int days, hours, mins, secs; days = leftTime / ( 60*60*24 ); leftTime = leftTime % ( 60*60*24 ); hours = leftTime / ( 60*60 ); leftTime = leftTime % ( 60*60 ); mins = leftTime / 60; secs = leftTime % 60; if ( days != 0 ) { ret = i18n( "d h m s", "%4d %3h %2m %1s" ) .arg( secs ) .arg( mins ) .arg( hours ) .arg( days ); } else if ( hours != 0 ) { ret = i18n( "h m s", "%3h %2m %1s" ) .arg( secs ) .arg( mins ) .arg( hours ); } else { ret = i18n( "m s", "%2m %1s" ) .arg( secs ) .arg( mins ); } } return ret; } void Kopete::Contact::slotBlock() { account()->block( d->contactId ); } void Kopete::Contact::slotUnblock() { account()->unblock( d->contactId ); } void Kopete::Contact::setNickName( const TQString &name ) { setProperty( Kopete::Global::Properties::self()->nickName(), name ); } TQString Kopete::Contact::nickName() const { TQString nick = property( Kopete::Global::Properties::self()->nickName() ).value().toString(); if( !nick.isEmpty() ) return nick; return contactId(); } void Contact::virtual_hook( uint , void * ) { } } //END namespace Kopete #include "kopetecontact.moc"