1 /*
2     kopetecontact.cpp - Kopete Contact
3 
4     Copyright (c) 2002-2004 by Duncan Mac-Vicar Prett <duncan@kde.org>
5     Copyright (c) 2002-2003 by Martijn Klingens       <klingens@kde.org>
6     Copyright (c) 2002-2004 by Olivier Goffart        <ogoffart @ kde.org>
7 
8     Kopete    (c) 2002-2004 by the Kopete developers  <kopete-devel@kde.org>
9 
10     *************************************************************************
11     *                                                                       *
12     * This library is free software; you can redistribute it and/or         *
13     * modify it under the terms of the GNU Lesser General Public            *
14     * License as published by the Free Software Foundation; either          *
15     * version 2 of the License, or (at your option) any later version.      *
16     *                                                                       *
17     *************************************************************************
18 */
19 
20 #include "kopetecontact.h"
21 
22 #include <QDialog>
23 #include <QIcon>
24 #include <QMenu>
25 #include <QLocale>
26 #include <QVariant>
27 #include <QWidget>
28 
29 #include <kabcpersistence.h>
30 #include <kmessagebox.h>
31 #include <kmessagebox_queued.h>
32 #include <ktreewidgetsearchline.h>
33 
34 #include "kopetecontactlist.h"
35 #include "kopeteglobal.h"
36 #include "kopeteuiglobal.h"
37 #include "kopeteprotocol.h"
38 #include "kopeteaccount.h"
39 #include "kopetestdaction.h"
40 #include "kopetechatsession.h"
41 #include "kopeteview.h"
42 #include "kopetemetacontact.h"
43 #include "kopeteappearancesettings.h"
44 #include "kopetebehaviorsettings.h"
45 #include "metacontactselectorwidget.h"
46 #include "kopeteemoticons.h"
47 #include "kopetestatusmessage.h"
48 #include "kopeteinfodialog.h"
49 #include "kopetedeletecontacttask.h"
50 
51 //For the moving to another metacontact dialog
52 #include <QVBoxLayout>
53 #include <QCheckBox>
54 #include <QDialogButtonBox>
55 #include <QPushButton>
56 
57 namespace Kopete {
58 class Contact::Private
59 {
60 public:
61     bool fileCapable;
62 
63     OnlineStatus onlineStatus;
64     Account *account;
65 
66     MetaContact *metaContact;
67 
68     QString contactId;
69     QString icon;
70 
71     QTime idleTimer;
72     unsigned long int idleTime;
73 
74     Kopete::StatusMessage statusMessage;
75     KToggleAction *toggleAlwaysVisibleAction;
76 
77     Kopete::Contact::NameType preferredNameType;
78     QString oldName;
79 };
80 
81 /* static */
nameTypeFromString(const QString & nameType)82 Kopete::Contact::NameType Kopete::Contact::nameTypeFromString(const QString &nameType)
83 {
84     if (nameType == QLatin1String("nickName")) {
85         return Kopete::Contact::NickName;
86     } else if (nameType == QLatin1String("customName")) {
87         return Kopete::Contact::CustomName;
88     } else if (nameType == QLatin1String("formattedName")) {
89         return Kopete::Contact::FormattedName;
90     } else if (nameType == QLatin1String("contactId")) {
91         return Kopete::Contact::ContactId;
92     } else { // fallback to custom name
93         return Kopete::Contact::CustomName;
94     }
95 }
96 
97 /* static */
nameTypeToString(Kopete::Contact::NameType nameType)98 const QString Kopete::Contact::nameTypeToString(Kopete::Contact::NameType nameType)
99 {
100     switch (nameType) {
101     case Kopete::Contact::NickName:
102         return QStringLiteral("nickName");
103     case Kopete::Contact::FormattedName:
104         return QStringLiteral("formattedName");
105     case Kopete::Contact::ContactId:
106         return QStringLiteral("contactId");
107     case Kopete::Contact::CustomName:
108     default:     // fallback to custom name
109         return QStringLiteral("customName");
110     }
111 }
112 
Contact(Account * account,const QString & contactId,MetaContact * parent,const QString & icon)113 Contact::Contact(Account *account, const QString &contactId, MetaContact *parent, const QString &icon)
114     : ContactListElement(parent)
115     , d(new Private())
116 {
117     //qCDebug(LIBKOPETE_LOG) << "Creating contact with id " << contactId;
118 
119     d->contactId = contactId;
120     d->metaContact = parent;
121     connect(d->metaContact, SIGNAL(destroyed(QObject*)), this, SLOT(slotMetaContactDestroyed(QObject*)));
122 
123     d->fileCapable = false;
124     d->account = account;
125     d->idleTime = 0;
126     d->icon = icon;
127     d->preferredNameType = Kopete::Contact::CustomName;
128     d->oldName = displayName();
129 
130     connect(this, SIGNAL(propertyChanged(Kopete::PropertyContainer*,QString,QVariant,QVariant)),
131             this, SLOT(slotPropertyChanged(Kopete::PropertyContainer*,QString,QVariant,QVariant)));
132 
133     bool duplicate = false;
134     // If can happend that a MetaContact may be used without a account
135     // (ex: for unit tests or chat window style preview)
136     if (account) {
137         // Don't register myself contacts because otherwise we can't have own contact in contact list.
138         if (d->metaContact != Kopete::ContactList::self()->myself()) {
139             duplicate = !account->registerContact(this);
140         }
141 
142         connect(account, SIGNAL(isConnectedChanged()), SLOT(slotAccountIsConnectedChanged()));
143     }
144 
145     // Need to check this because myself() may have no parent
146     // Maybe too the metaContact doesn't have a valid protocol()
147     // (ex: for unit tests or chat window style preview)
148 
149     // if alreadyRegistered is true (which mean that this is duplicate contact) we will not add
150     // parent and the contact will die out on next Kopete restart.
151     if (!duplicate && parent && protocol()) {
152         parent->addContact(this);
153     }
154 }
155 
~Contact()156 Contact::~Contact()
157 {
158     //qCDebug(LIBKOPETE_LOG) ;
159     emit(contactDestroyed(this));
160     delete d;
161 }
162 
onlineStatus() const163 OnlineStatus Contact::onlineStatus() const
164 {
165     if (this == account()->myself() || account()->isConnected()) {
166         return d->onlineStatus;
167     } else {
168         return protocol()->accountOfflineStatus();
169     }
170 }
171 
setOnlineStatus(const OnlineStatus & status)172 void Contact::setOnlineStatus(const OnlineStatus &status)
173 {
174     if (status == d->onlineStatus) {
175         return;
176     }
177 
178     const bool oldCanAcceptFiles = canAcceptFiles();
179     OnlineStatus oldStatus = d->onlineStatus;
180     d->onlineStatus = status;
181 
182     Kopete::Global::Properties *globalProps = Kopete::Global::Properties::self();
183 
184     // Contact changed from Offline to another known online status
185     if (oldStatus.status() == OnlineStatus::Offline
186         && status.status() != OnlineStatus::Unknown
187         && status.status() != OnlineStatus::Offline) {
188         if (!hasProperty(globalProps->onlineSince().key())) {
189             setProperty(globalProps->onlineSince(), QDateTime::currentDateTime());
190         }
191         // qCDebug(LIBKOPETE_LOG) << "REMOVING lastSeen property for " << nickName();
192         removeProperty(globalProps->lastSeen());
193     } else if (oldStatus.status() != OnlineStatus::Offline
194                && oldStatus.status() != OnlineStatus::Unknown
195                && status.status() == OnlineStatus::Offline) { // Contact went back offline
196         removeProperty(globalProps->onlineSince());
197         // qCDebug(LIBKOPETE_LOG) << "SETTING lastSeen property for " << nickName();
198         setProperty(globalProps->lastSeen(), QDateTime::currentDateTime());
199     }
200 
201     if (this == account()->myself() || account()->isConnected()) {
202         emit onlineStatusChanged(this, status, oldStatus);
203     }
204 
205     if (oldCanAcceptFiles != canAcceptFiles()) {
206         emit canAcceptFilesChanged();
207     }
208 }
209 
statusMessage() const210 Kopete::StatusMessage Contact::statusMessage() const
211 {
212     return d->statusMessage;
213 }
214 
setStatusMessage(const Kopete::StatusMessage & statusMessage)215 void Contact::setStatusMessage(const Kopete::StatusMessage &statusMessage)
216 {
217     bool emitUpdate = true;
218 
219     if (d->statusMessage.title() == statusMessage.title() && d->statusMessage.message() == statusMessage.message()) {
220         emitUpdate = false;
221     }
222 
223     d->statusMessage = statusMessage;
224 
225     qCDebug(LIBKOPETE_LOG) << "Setting up the status title property with this: " << statusMessage.title();
226     if (!statusMessage.title().isEmpty()) {
227         setProperty(Kopete::Global::Properties::self()->statusTitle(), statusMessage.title());
228     } else {
229         removeProperty(Kopete::Global::Properties::self()->statusTitle());
230     }
231 
232     qCDebug(LIBKOPETE_LOG) << "Setting up the status message property with this: " << statusMessage.message();
233     if (!statusMessage.message().isEmpty()) {
234         setProperty(Kopete::Global::Properties::self()->statusMessage(), statusMessage.message());
235     } else {
236         removeProperty(Kopete::Global::Properties::self()->statusMessage());
237     }
238 
239     if (emitUpdate) {
240         emit statusMessageChanged(this);
241     }
242 }
243 
slotAccountIsConnectedChanged()244 void Contact::slotAccountIsConnectedChanged()
245 {
246     if (this == account()->myself()) {
247         return;
248     }
249 
250     if (account()->isConnected()) {
251         emit onlineStatusChanged(this, d->onlineStatus, protocol()->accountOfflineStatus());
252     } else {
253         emit onlineStatusChanged(this, protocol()->accountOfflineStatus(), d->onlineStatus);
254     }
255 }
256 
sendFile(const QUrl &,const QString &,uint)257 void Contact::sendFile(const QUrl &, const QString &, uint)
258 {
259     qCWarning(LIBKOPETE_LOG) << "Plugin "
260                              << protocol()->pluginId() << " has enabled file sending, "
261                              << "but didn't implement it!" << endl;
262 }
263 
slotAddContact()264 void Contact::slotAddContact()
265 {
266     if (metaContact()) {
267         metaContact()->setTemporary(false);
268         ContactList::self()->addMetaContact(metaContact());
269     }
270 }
271 
popupMenu(ChatSession *)272 QMenu *Contact::popupMenu(ChatSession *)
273 {
274     return popupMenu();
275 }
276 
popupMenu()277 QMenu *Contact::popupMenu()
278 {
279     QMenu *menu = new QMenu();
280 
281     QString titleText;
282     const QString nick = displayName();
283     if (nick == contactId()) {
284         titleText = QStringLiteral("%1 (%2)").arg(contactId(), onlineStatus().description());
285     } else {
286         titleText = QStringLiteral("%1 <%2> (%3)").arg(nick, contactId(), onlineStatus().description());
287     }
288     menu->addSection(titleText);
289 
290     if (metaContact() && metaContact()->isTemporary() && contactId() != account()->myself()->contactId()) {
291         QAction *actionAddContact = new QAction(QIcon::fromTheme(QStringLiteral("list-add-user")), i18n("&Add to Your Contact List"), menu);
292         connect(actionAddContact, SIGNAL(triggered(bool)), this, SLOT(slotAddContact()));
293 
294         menu->addAction(actionAddContact);
295         menu->addSeparator();
296     }
297 
298     // FIXME: After KDE 3.2 we should make isReachable do the isConnected call so it can be removed here - Martijn
299     const bool reach = account()->isConnected() && isReachable();
300     const bool myself = (this == account()->myself());
301 
302     QAction *actionSendMessage = KopeteStdAction::sendMessage(this, SLOT(sendMessage()), menu);
303     actionSendMessage->setEnabled(reach && !myself);
304     menu->addAction(actionSendMessage);
305 
306     QAction *actionChat = KopeteStdAction::chat(this, SLOT(startChat()), menu);
307     actionChat->setEnabled(reach && !myself);
308     menu->addAction(actionChat);
309 
310     QAction *actionSendFile = KopeteStdAction::sendFile(this, SLOT(sendFile()), menu);
311     actionSendFile->setEnabled(reach && d->fileCapable && !myself);
312     menu->addAction(actionSendFile);
313 
314     // Protocol specific options will go below this separator
315     // through the use of the customContextMenuActions() function
316 
317     // Get the custom actions from the protocols ( pure virtual function )
318     QList<QAction *> *customActions = customContextMenuActions();
319     if (customActions && !customActions->isEmpty()) {
320         menu->addSeparator();
321         QList<QAction *>::iterator it, itEnd = customActions->end();
322         for (it = customActions->begin(); it != itEnd; ++it) {
323             menu->addAction((*it));
324         }
325     }
326     delete customActions;
327 
328     menu->addSeparator();
329 
330     if (metaContact() && !metaContact()->isTemporary()) {
331         QAction *changeMetaContact = KopeteStdAction::changeMetaContact(this, SLOT(changeMetaContact()), menu);
332         menu->addAction(changeMetaContact);
333 
334         d->toggleAlwaysVisibleAction = new KToggleAction(i18n("Visible when offline"), menu);
335         d->toggleAlwaysVisibleAction->setChecked(property(Kopete::Global::Properties::self()->isAlwaysVisible()).value().toBool());
336         menu->addAction(d->toggleAlwaysVisibleAction);
337         connect(d->toggleAlwaysVisibleAction, SIGNAL(toggled(bool)), this, SLOT(toggleAlwaysVisible()));
338     }
339 
340     menu->addAction(KopeteStdAction::contactInfo(this, SLOT(slotUserInfo()), menu));
341 
342 #if 0 //this is not fully implemented yet (and doesn't work).  disable for now   - Olivier 2005-01-11
343     if (account()->isBlocked(d->contactId)) {
344         KopeteStdAction::unblockContact(this, SLOT(slotUnblock()), menu, "actionUnblockContact")->plug(menu);
345     } else {
346         KopeteStdAction::blockContact(this, SLOT(slotBlock()), menu, "actionBlockContact")->plug(menu);
347     }
348 #endif
349 
350     if (metaContact() && !metaContact()->isTemporary()) {
351         menu->addAction(KopeteStdAction::deleteContact(this, SLOT(slotDelete()), menu));
352     }
353 
354     return menu;
355 }
356 
toggleAlwaysVisible()357 void Contact::toggleAlwaysVisible()
358 {
359     bool alwaysVisible = property(Kopete::Global::Properties::self()->isAlwaysVisible()).value().toBool();
360     setProperty(Kopete::Global::Properties::self()->isAlwaysVisible(), !alwaysVisible);
361     d->toggleAlwaysVisibleAction->setChecked(!alwaysVisible);
362 }
363 
changeMetaContact()364 void Contact::changeMetaContact()
365 {
366     QPointer <QDialog> moveDialog = new QDialog(Kopete::UI::Global::mainWidget());
367     moveDialog->setWindowTitle(i18n("Move Contact"));
368     QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel);
369     QWidget *mainWidget = new QWidget(Kopete::UI::Global::mainWidget());
370     QVBoxLayout *mainLayout = new QVBoxLayout;
371     moveDialog->setLayout(mainLayout);
372     mainLayout->addWidget(mainWidget);
373     QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
374     okButton->setDefault(true);
375     okButton->setShortcut(Qt::CTRL | Qt::Key_Return);
376     moveDialog->connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept()));
377     moveDialog->connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject()));
378     //PORTING SCRIPT: WARNING mainLayout->addWidget(buttonBox) must be last item in layout. Please move it.
379     mainLayout->addWidget(buttonBox);
380     buttonBox->button(QDialogButtonBox::Ok)->setDefault(true);
381 
382     QWidget *w = new QWidget(moveDialog);
383     QVBoxLayout *wVBoxLayout = new QVBoxLayout(w);
384     wVBoxLayout->setMargin(0);
385 //TODO PORT QT5     wVBoxLayout->setSpacing( QDialog::spacingHint() );
386     Kopete::UI::MetaContactSelectorWidget *selector = new Kopete::UI::MetaContactSelectorWidget(w);
387     selector->setLabelMessage(i18n("Select the meta contact to which you want to move this contact:"));
388     // exclude this metacontact as a target metacontact for the move
389     selector->excludeMetaContact(metaContact());
390     QCheckBox *chkCreateNew = new QCheckBox(i18n("Create a new metacontact for this contact"), w);
391     wVBoxLayout->addWidget(chkCreateNew);
392     chkCreateNew->setWhatsThis(i18n("If you select this option, a new metacontact will be created in the top-level group "
393                                     "with the name of this contact and the contact will be moved to it."));
394     QObject::connect(chkCreateNew, SIGNAL(toggled(bool)), selector, SLOT(setDisabled(bool)));
395 
396     mainLayout->addWidget(w);
397     if (moveDialog->exec() == QDialog::Accepted) {
398         Kopete::MetaContact *mc = selector->metaContact();
399         if (chkCreateNew->isChecked()) {
400             mc = new Kopete::MetaContact();
401 
402             if (metaContact()) { // Add new metaContact to old groups so we don't move it to Top Level group
403                 foreach (Kopete::Group *group, metaContact()->groups()) {
404                     mc->addToGroup(group);
405                 }
406             }
407 
408             Kopete::ContactList::self()->addMetaContact(mc);
409         }
410         if (mc) {
411             setMetaContact(mc);
412         }
413     }
414 
415     if (moveDialog) {
416         moveDialog->deleteLater();
417     }
418 }
419 
slotMetaContactDestroyed(QObject * mc)420 void Contact::slotMetaContactDestroyed(QObject *mc)
421 {
422     if (mc != d->metaContact) {
423         return;
424     }
425 
426     d->metaContact = 0;
427 }
428 
setMetaContact(MetaContact * m)429 void Contact::setMetaContact(MetaContact *m)
430 {
431     MetaContact *old = d->metaContact;
432     if (old == m) { //that make no sens
433         return;
434     }
435 
436     if (old) {
437         old->removeContact(this);
438         disconnect(old, SIGNAL(destroyed(QObject*)), this, SLOT(slotMetaContactDestroyed(QObject*)));
439 
440         if (old->contacts().isEmpty()) {
441             //remove the old metacontact.  (this delete the MC)
442             ContactList::self()->removeMetaContact(old);
443         } else {
444             d->metaContact = m; //i am forced to do that now if i want the next line works
445             //remove cached data for this protocol which will not be removed since we disconnected
446             protocol()->serialize(old);
447         }
448     }
449 
450     d->metaContact = m;
451     setParent(m);
452 
453     if (m) {
454         m->addContact(this);
455         connect(m, SIGNAL(destroyed(QObject*)), this, SLOT(slotMetaContactDestroyed(QObject*)));
456         // it is necessary to call this write here, because MetaContact::addContact() does not differentiate
457         // between adding completely new contacts (which should be written to kabc) and restoring upon restart
458         // (where no write is needed).
459         KABCPersistence::self()->write(m);
460     }
461     sync();
462 }
463 
serialize(QMap<QString,QString> &,QMap<QString,QString> &)464 void Contact::serialize(QMap<QString, QString> & /*serializedData*/, QMap<QString, QString> & /* addressBookData */)
465 {
466 }
467 
isReachable()468 bool Contact::isReachable()
469 {
470     // The default implementation returns false when offline and true
471     // otherwise. Subclass if you need more control over the process.
472     return onlineStatus().status() != OnlineStatus::Offline;
473 }
474 
startChat()475 void Contact::startChat()
476 {
477     KopeteView *v = manager(CanCreate)->view(true, QStringLiteral("kopete_chatwindow"));
478     if (v) {
479         v->raise(true);
480     }
481 }
482 
sendMessage()483 void Contact::sendMessage()
484 {
485     KopeteView *v = manager(CanCreate)->view(true, QStringLiteral("kopete_emailwindow"));
486     if (v) {
487         v->raise(true);
488     }
489 }
490 
execute()491 void Contact::execute()
492 {
493     // FIXME: After KDE 3.2 remove the isConnected check and move it to isReachable - Martijn
494     if (account()->isConnected() && isReachable()) {
495         KopeteView *v = manager(CanCreate)->view(true, Kopete::BehaviorSettings::self()->viewPlugin());
496         if (v) {
497             v->raise(true);
498         }
499     } else {
500         KMessageBox::queuedMessageBox(Kopete::UI::Global::mainWidget(), KMessageBox::Sorry,
501                                       i18n("This user is not reachable at the moment. Please try a protocol that supports offline sending, or wait "
502                                            "until this user comes online."), i18n("User is Not Reachable"));
503     }
504 }
505 
slotDelete()506 void Contact::slotDelete()
507 {
508     if (KMessageBox::warningContinueCancel(Kopete::UI::Global::mainWidget(),
509                                            i18n("Are you sure you want to remove the contact  '%1' from your contact list?",
510                                                 d->contactId), i18n("Remove Contact"), KGuiItem(i18n("Remove"), QLatin1String("list-remove-user")), KStandardGuiItem::cancel(),
511                                            QStringLiteral("askRemoveContact"), KMessageBox::Notify)
512         == KMessageBox::Continue) {
513         Kopete::DeleteContactTask *deleteTask = new Kopete::DeleteContactTask(this);
514         deleteTask->start();
515     }
516 }
517 
deleteContact()518 void Contact::deleteContact()
519 {
520     // Default implementation simply deletes the contact
521     deleteLater();
522 }
523 
metaContact() const524 MetaContact *Contact::metaContact() const
525 {
526     return d->metaContact;
527 }
528 
contactId() const529 QString Contact::contactId() const
530 {
531     return d->contactId;
532 }
533 
protocol() const534 Protocol *Contact::protocol() const
535 {
536     return d->account ? d->account->protocol() : 0L;
537 }
538 
account() const539 Account *Contact::account() const
540 {
541     return d->account;
542 }
543 
sync(unsigned int)544 void Contact::sync(unsigned int)
545 {
546     /* Default implementation does nothing */
547 }
548 
icon() const549 QString &Contact::icon() const
550 {
551     return d->icon;
552 }
553 
setIcon(const QString & icon)554 void Contact::setIcon(const QString &icon)
555 {
556     d->icon = icon;
557 }
558 
customContextMenuActions()559 QList<QAction *> *Contact::customContextMenuActions()
560 {
561     return nullptr;
562 }
563 
customContextMenuActions(ChatSession *)564 QList<QAction *> *Contact::customContextMenuActions(ChatSession * /* manager */)
565 {
566     return customContextMenuActions();
567 }
568 
isOnline() const569 bool Contact::isOnline() const
570 {
571     return onlineStatus().isDefinitelyOnline();
572 }
573 
isFileCapable() const574 bool Contact::isFileCapable() const
575 {
576     return d->fileCapable;
577 }
578 
setFileCapable(bool filecap)579 void Contact::setFileCapable(bool filecap)
580 {
581     if (d->fileCapable != filecap) {
582         d->fileCapable = filecap;
583         emit canAcceptFilesChanged();
584     }
585 }
586 
canAcceptFiles() const587 bool Contact::canAcceptFiles() const
588 {
589     return isOnline() && d->fileCapable;
590 }
591 
idleTime() const592 unsigned long int Contact::idleTime() const
593 {
594     if (d->idleTime == 0) {
595         return 0;
596     }
597 
598     return d->idleTime+(d->idleTimer.elapsed()/1000);
599 }
600 
setIdleTime(unsigned long int t)601 void Contact::setIdleTime(unsigned long int t)
602 {
603     bool idleChanged = false;
604     if (d->idleTime != t) {
605         idleChanged = true;
606     }
607     d->idleTime = t;
608     if (t > 0) {
609         d->idleTimer.start();
610     }
611 //FIXME: if t == 0, idleTime() will now return garbage
612 //	else
613 //		d->idleTimer.stop();
614     if (idleChanged) {
615         emit idleStateChanged(this);
616     }
617 }
618 
toolTip() const619 QString Contact::toolTip() const
620 {
621     Kopete::Property p;
622     QString tip;
623     const QStringList shownProps = Kopete::AppearanceSettings::self()->toolTipContents();
624 
625     // --------------------------------------------------------------------------
626     // Fixed part of tooltip
627 
628     QString iconName;
629     if (this == account()->myself()) {
630         iconName = QStringLiteral("kopete-account-icon:%1:%2")
631                    .arg(QString(QUrl::toPercentEncoding(protocol()->pluginId())),
632                         QString(QUrl::toPercentEncoding(account()->accountId())));
633     } else {
634         iconName = QStringLiteral("kopete-contact-icon:%1:%2:%3")
635                    .arg(QString(QUrl::toPercentEncoding(protocol()->pluginId())),
636                         QString(QUrl::toPercentEncoding(account()->accountId())),
637                         QString(QUrl::toPercentEncoding(contactId())));
638     }
639 
640     QString nick = displayName();
641     if (nick == contactId()) {
642         tip = i18nc("@label:textbox %3 is contact-display-name, %1 is its status",
643                     "<b><nobr>%3</nobr></b><br /><img src=\"%2\">&nbsp;%1",
644                     Kopete::Message::escape(onlineStatus().description()), iconName,
645                     Kopete::Message::escape(d->contactId));
646     } else {
647         tip = i18nc("@label:textbox %4 is contact-display-name, %3 is contact-id, %1 is its status",
648                     "<nobr><b>%4</b> (%3)</nobr><br /><img src=\"%2\">&nbsp;%1",
649                     Kopete::Message::escape(onlineStatus().description()), iconName,
650                     Kopete::Message::escape(contactId()),
651                     Kopete::Emoticons::parseEmoticons(Kopete::Message::escape(nick)));
652     }
653 
654     // --------------------------------------------------------------------------
655     // Configurable part of tooltip
656 
657     // FIXME: It shouldn't use QString to identity the properties. Instead it should use PropertyTmpl::key()
658     for (QStringList::ConstIterator it = shownProps.constBegin(); it != shownProps.constEnd(); ++it) {
659         if ((*it) == Kopete::Global::Properties::self()->fullName().key()) {
660             const QString name = formattedName();
661             if (!name.isEmpty()) {
662                 tip += i18nc("@label:textbox formatted name",
663                              "<br /><b>Full Name:</b>&nbsp;<nobr>%1</nobr>", name.toHtmlEscaped());
664             }
665         } else if ((*it) == Kopete::Global::Properties::self()->idleTime().key()) {
666             const QString time = formattedIdleTime();
667             if (!time.isEmpty()) {
668                 tip += i18nc("@label:textbox formatted idle time",
669                              "<br /><b>Idle:</b>&nbsp;<nobr>%1</nobr>", time);
670             }
671         } else if ((*it) == QLatin1String("homePage")) {
672             const QString url = property(*it).value().toString();
673             if (!url.isEmpty()) {
674                 tip += i18nc("@label:textbox formatted url",
675                              "<br /><b>Home Page:</b>&nbsp;<a href=\"%1\"><nobr>%2</nobr></a>",
676                              QString(QUrl::toPercentEncoding(url)), Kopete::Message::escape(url.toHtmlEscaped()));
677             }
678         } else if ((*it) == Kopete::Global::Properties::self()->statusTitle().key()) {
679             const QString statusTitle = property(*it).value().toString();
680             if (!statusTitle.isEmpty()) {
681                 tip += i18nc("@label:textbox formatted status title",
682                              "<br /><b>Status&nbsp;Title:</b>&nbsp;%1", Kopete::Emoticons::parseEmoticons(Kopete::Message::escape(statusTitle)));
683             }
684         } else if ((*it) == Kopete::Global::Properties::self()->statusMessage().key()) {
685             const QString statusmsg = property(*it).value().toString();
686             if (!statusmsg.isEmpty()) {
687                 tip += i18nc("@label:textbox formatted status message",
688                              "<br /><b>Status&nbsp;Message:</b>&nbsp;%1", Kopete::Emoticons::parseEmoticons(Kopete::Message::escape(statusmsg)));
689             }
690         } else {
691             p = property(*it);
692             if (!p.isNull()) {
693                 QVariant val = p.value();
694                 QString valueText;
695 
696                 switch (val.type()) {
697                 case QMetaType::QDateTime:
698                     valueText = QLocale().toString(val.toDateTime());
699                     valueText = Kopete::Message::escape(valueText);
700                     break;
701                 case QMetaType::QDate:
702                     valueText = QLocale().toString(val.toDate());
703                     valueText = Kopete::Message::escape(valueText);
704                     break;
705                 case QMetaType::QTime:
706                     valueText = QLocale().toString(val.toTime());
707                     valueText = Kopete::Message::escape(valueText);
708                     break;
709                 default:
710                     if (p.isRichText()) {
711                         valueText = val.toString();
712                     } else {
713                         valueText = Kopete::Message::escape(val.toString());
714                     }
715                 }
716 
717                 if (valueText.size() > 1000) {
718                     valueText.truncate(997);
719                     valueText += QLatin1String("...");
720                 }
721 
722                 tip += i18nc("@label:textbox property label %2 is name, %1 is value",
723                              "<br /><nobr><b>%2:</b></nobr>&nbsp;%1",
724                              valueText, p.tmpl().label().toHtmlEscaped());
725             }
726         }
727     }
728 
729     return tip;
730 }
731 
formattedName() const732 QString Kopete::Contact::formattedName() const
733 {
734     if (hasProperty(Kopete::Global::Properties::self()->fullName().key())) {
735         return property(Kopete::Global::Properties::self()->fullName()).value().toString();
736     }
737 
738     QString ret;
739     Kopete::Property first, last;
740 
741     first = property(Kopete::Global::Properties::self()->firstName());
742     last = property(Kopete::Global::Properties::self()->lastName());
743     if (!first.isNull()) {
744         if (!last.isNull()) { // contact has both first and last name
745             ret = i18nc("firstName lastName", "%2 %1",
746                         last.value().toString(),
747                         first.value().toString());
748         } else { // only first name set
749             ret = first.value().toString();
750         }
751     } else if (!last.isNull()) { // only last name set
752         ret = last.value().toString();
753     }
754 
755     return ret;
756 }
757 
formattedIdleTime() const758 QString Kopete::Contact::formattedIdleTime() const
759 {
760     QString ret;
761     unsigned long int leftTime = idleTime();
762 
763     if (leftTime > 0) { // FIXME: duplicated from code in kopetecontact listview.cpp
764         unsigned long int days, hours, mins, secs;
765 
766         days = leftTime / (60*60*24);
767         leftTime = leftTime % (60*60*24);
768         hours = leftTime / (60*60);
769         leftTime = leftTime % (60*60);
770         mins = leftTime / 60;
771         secs = leftTime % 60;
772 
773         if (days != 0) {
774             ret = i18nc("<days>d <hours>h <minutes>m <seconds>s",
775                         "%4d %3h %2m %1s",
776                         secs,
777                         mins,
778                         hours,
779                         days);
780         } else if (hours != 0) {
781             ret = i18nc("<hours>h <minutes>m <seconds>s", "%3h %2m %1s",
782                         secs,
783                         mins,
784                         hours);
785         } else {
786             // xgettext: no-c-format
787             ret = i18nc("<minutes>m <seconds>s", "%2m %1s",
788                         secs,
789                         mins);
790         }
791     }
792     return ret;
793 }
794 
slotBlock()795 void Kopete::Contact::slotBlock()
796 {
797     account()->block(d->contactId);
798 }
799 
slotUnblock()800 void Kopete::Contact::slotUnblock()
801 {
802     account()->unblock(d->contactId);
803 }
804 
setNickName(const QString & name)805 void Kopete::Contact::setNickName(const QString &name)
806 {
807     setProperty(Kopete::Global::Properties::self()->nickName(), name);
808 }
809 
nickName() const810 QString Kopete::Contact::nickName() const
811 {
812     const QString nick = property(Kopete::Global::Properties::self()->nickName()).value().toString();
813     if (!nick.isEmpty()) {
814         return nick;
815     }
816 
817     return contactId();
818 }
819 
setCustomName(const QString & name)820 void Kopete::Contact::setCustomName(const QString &name)
821 {
822     setProperty(Kopete::Global::Properties::self()->customName(), name);
823 }
824 
customName() const825 QString Kopete::Contact::customName() const
826 {
827     const QString name = property(Kopete::Global::Properties::self()->customName()).value().toString();
828     if (!name.isEmpty()) {
829         return name;
830     }
831     return nickName();
832 }
833 
setPhoto(const QString & photoPath)834 void Kopete::Contact::setPhoto(const QString &photoPath)
835 {
836     setProperty(Kopete::Global::Properties::self()->photo(), photoPath);
837 }
838 
slotPropertyChanged(Kopete::PropertyContainer *,const QString & key,const QVariant &,const QVariant &)839 void Kopete::Contact::slotPropertyChanged(Kopete::PropertyContainer *, const QString &key, const QVariant &, const QVariant &)
840 {
841     if (key != Kopete::Global::Properties::self()->customName().key()
842         && key != Kopete::Global::Properties::self()->fullName().key()
843         && key != Kopete::Global::Properties::self()->firstName().key()
844         && key != Kopete::Global::Properties::self()->lastName().key()
845         && key != Kopete::Global::Properties::self()->nickName().key()) {
846         return;
847     }
848 
849     const QString oldName = d->oldName;
850     const QString newName = displayName();
851     if (oldName != newName) {
852         d->oldName = newName;
853         emit displayNameChanged(oldName, newName);
854     }
855 }
856 
setPreferredNameType(Kopete::Contact::NameType preferredNameType)857 void Kopete::Contact::setPreferredNameType(Kopete::Contact::NameType preferredNameType)
858 {
859     if (d->preferredNameType != preferredNameType) {
860         const QString oldName = displayName();
861         d->preferredNameType = preferredNameType;
862         const QString newName = displayName();
863         if (oldName != newName) {
864             d->oldName = newName;
865             emit displayNameChanged(oldName, newName);
866         }
867     }
868 }
869 
preferredNameType() const870 Kopete::Contact::NameType Kopete::Contact::preferredNameType() const
871 {
872     return d->preferredNameType;
873 }
874 
displayName() const875 QString Kopete::Contact::displayName() const
876 {
877     QString name;
878     switch (d->preferredNameType) {
879     case NickName:
880         name = nickName();
881         break;
882     case FormattedName:
883         name = formattedName();
884         break;
885     case ContactId:
886         name = contactId();
887         break;
888     case CustomName:
889     default:     // fallback to custom name
890         name = customName();
891         break;
892     }
893     if (name.isEmpty()) {
894         return contactId();
895     } else {
896         return name;
897     }
898 }
899 } //END namespace Kopete
900