1 /*
2  * This file is part of Licq, an instant messaging client for UNIX.
3  * Copyright (C) 2007-2014 Licq developers <licq-dev@googlegroups.com>
4  *
5  * Licq is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * Licq is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with Licq; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18  */
19 
20 #include "contactuserdata.h"
21 
22 // Standard
23 #include <climits>
24 #include <cstring>
25 
26 // Qt
27 #include <QDateTime>
28 #include <QImage>
29 
30 // Licq
31 #include <licq/contactlist/user.h>
32 #include <licq/icq/user.h>
33 #include <licq/plugin/pluginmanager.h>
34 #include <licq/pluginsignal.h>
35 #include <licq/socket.h>
36 #include <licq/userevents.h>
37 
38 // Qt-gui
39 #include "config/contactlist.h"
40 
41 #include "core/gui-defines.h"
42 
43 #include "contactgroup.h"
44 #include "contactuser.h"
45 
46 using namespace LicqQtGui;
47 /* TRANSLATOR LicqQtGui::ContactUserData */
48 
49 using std::string;
50 using Licq::User;
51 
52 #define FLASH_TIME 500
53 
54 // Can't initialize timers here in static context so set to zero and let first object take care of initialization
55 QTimer* ContactUserData::myRefreshTimer = NULL;
56 QTimer* ContactUserData::myAnimateTimer = NULL;
57 
58 int ContactUserData::myAnimatorCount = 0;
59 
60 
ContactUserData(const Licq::User * licqUser,QObject * parent)61 ContactUserData::ContactUserData(const Licq::User* licqUser, QObject* parent)
62   : myStatus(User::OfflineStatus),
63     myEvents(0),
64     myFlash(false),
65     mySubGroup(ContactListModel::OfflineSubGroup),
66     myVisibility(false),
67     myOnlCounter(0),
68     myCarCounter(0),
69     myAnimating(false),
70     myUserIcon(NULL)
71 {
72   myUserId = licqUser->id();
73 
74   if (myRefreshTimer == NULL)
75   {
76     // Create the static timer used to update dynamic contents
77     myRefreshTimer = new QTimer(parent);
78     myRefreshTimer->start(60 * 1000);
79   }
80   connect(myRefreshTimer, SIGNAL(timeout()), SLOT(refresh()));
81 
82   // Create the static timer used for animations
83   if (myAnimateTimer == NULL)
84   {
85     myAnimateTimer = new QTimer(parent);
86     myAnimateTimer->setInterval(FLASH_TIME);
87   }
88 
89   update(licqUser, 0);
90 }
91 
~ContactUserData()92 ContactUserData::~ContactUserData()
93 {
94   // Free up animation timer resource if we were using it
95   if (myFlash || myOnlCounter > 0 || myCarCounter > 0)
96     stopAnimation();
97 
98   // Remove this user from all groups
99   while (!myUserInstances.isEmpty())
100     delete myUserInstances.takeFirst();
101 
102   if (myUserIcon != NULL)
103     delete myUserIcon;
104 }
105 
update(unsigned long subSignal,int argument)106 void ContactUserData::update(unsigned long subSignal, int argument)
107 {
108   if (subSignal == Licq::PluginSignal::UserEvents && argument == 0)
109   {
110     // User fetched our auto response message
111     myCarCounter = ((5*1000/FLASH_TIME)+1)&(-2);
112     startAnimation();
113     return;
114   }
115 
116   if (subSignal == Licq::PluginSignal::UserStatus && argument == 1)
117   {
118     // User came online
119     myOnlCounter = 5*1000/FLASH_TIME; // run about 5 seconds
120     startAnimation();
121     // Fall trough to actually update status
122   }
123 
124   Licq::UserReadGuard u(myUserId);
125   if (!u.isLocked())
126     return;
127 
128   update(*u, subSignal);
129 }
130 
update(const Licq::User * u,unsigned long subSignal)131 void ContactUserData::update(const Licq::User* u, unsigned long subSignal)
132 {
133   // Save some old values so we know if we got changes to signal
134   ContactListModel::SubGroupType oldSubGroup = mySubGroup;
135   bool oldVisibility = myVisibility;
136 
137   if (subSignal == 0 || subSignal == Licq::PluginSignal::UserStatus)
138   {
139     myStatus = u->status();
140     myStatusInvisible = u->isInvisible();
141     myTouched = u->Touched();
142   }
143 
144   if (subSignal == 0 || subSignal == Licq::PluginSignal::UserTyping)
145     myStatusTyping = u->isTyping();
146 
147 
148   if (subSignal == 0 || subSignal == Licq::PluginSignal::UserInfo)
149   {
150     myBirthday = (u->Birthday() == 0);
151     myPhone = !u->getUserInfoString("PhoneNumber").empty();
152     myCellular = !u->getCellularNumber().empty();
153   }
154 
155   if (subSignal == 0 || subSignal == Licq::PluginSignal::UserSecurity)
156   {
157     mySecure = u->Secure();
158     myGPGKey = !u->gpgKey().empty();
159     myGPGKeyEnabled = u->UseGPG();
160   }
161 
162   if (subSignal == 0 || subSignal == Licq::PluginSignal::UserSettings)
163   {
164     myCustomAR = !u->customAutoResponse().empty();
165     myNotInList = u->NotInList();
166     myNewUser = u->NewUser();
167     myAwaitingAuth = u->GetAwaitingAuth();
168     myInIgnoreList = u->IgnoreList();
169     myInOnlineNotify = u->OnlineNotify();
170     myInInvisibleList = u->InvisibleList();
171     myInVisibleList = u->VisibleList();
172   }
173 
174   if (myUserId.protocolId() == ICQ_PPID)
175   {
176     const Licq::IcqUser* icquser = dynamic_cast<const Licq::IcqUser*>(u);
177 
178     if (subSignal == 0 || subSignal == Licq::PluginSignal::UserPluginStatus)
179     {
180       myPhoneFollowMeStatus = icquser->phoneFollowMeStatus();
181       myIcqPhoneStatus = icquser->icqPhoneStatus();
182       mySharedFilesStatus = icquser->sharedFilesStatus();
183     }
184   }
185   else
186   {
187     myPhoneFollowMeStatus = Licq::IcqPluginInactive;
188     myIcqPhoneStatus = Licq::IcqPluginInactive;
189     mySharedFilesStatus = Licq::IcqPluginInactive;
190   }
191 
192   updateExtendedStatus();
193 
194   if (subSignal == 0 || subSignal == Licq::PluginSignal::UserEvents)
195     updateEvents(u);
196 
197   if (subSignal == 0 || subSignal == Licq::PluginSignal::UserPicture)
198     updatePicture(u);
199 
200   if (subSignal != Licq::PluginSignal::UserGroups &&
201       subSignal != Licq::PluginSignal::UserPicture &&
202       subSignal != Licq::PluginSignal::UserTyping &&
203       subSignal != Licq::PluginSignal::UserSecurity)
204   {
205     if (myNotInList)
206       mySubGroup = ContactListModel::NotInListSubGroup;
207     else if (myStatus == User::OfflineStatus)
208       mySubGroup = ContactListModel::OfflineSubGroup;
209     else
210       mySubGroup = ContactListModel::OnlineSubGroup;
211 
212     updateText(u);
213     updateSorting();
214     updateVisibility();
215   }
216 
217   // Note: When we get called from constructor, noone is connected to our signals
218   //       and myUserInstances is empty so below code won't trigger anything strange
219 
220   // Signal our own data changes before starting to touch groups and bars
221   if (subSignal != Licq::PluginSignal::UserGroups)
222     emit dataChanged(this);
223 
224   // If status has changed update the sub groups of all groups
225   if (mySubGroup != oldSubGroup)
226     foreach (ContactUser* user, myUserInstances)
227       user->group()->updateSubGroup(oldSubGroup, mySubGroup, myEvents);
228 
229   // Update group visibility
230   if (myVisibility != oldVisibility)
231     foreach (ContactUser* user, myUserInstances)
232       user->group()->updateVisibility(myVisibility, mySubGroup);
233 
234   // Add/remove us to/from groups
235   if (subSignal == 0 || subSignal == Licq::PluginSignal::UserSettings || subSignal == Licq::PluginSignal::UserGroups)
236     // Group membership is handled by ContactList so send it a signal to update
237     emit updateUserGroups(this, u);
238 }
239 
updatePicture(const Licq::User * u)240 void ContactUserData::updatePicture(const Licq::User* u)
241 {
242   if (myUserIcon != NULL)
243   {
244     delete myUserIcon;
245     myUserIcon = NULL;
246   }
247 
248   if (u->GetPicturePresent())
249   {
250     myUserIcon = new QImage(QString::fromLocal8Bit(u->pictureFileName().c_str()));
251     if (myUserIcon->isNull())
252     {
253       delete myUserIcon;
254       myUserIcon = NULL;
255     }
256   }
257 }
258 
updateEvents(const Licq::User * u)259 void ContactUserData::updateEvents(const Licq::User* u)
260 {
261   myUrgent = false;
262   myNewMessages = u->NewMessages();
263   if (myEvents != myNewMessages)
264   {
265     foreach (ContactUser* user, myUserInstances)
266       user->group()->updateNumEvents(myNewMessages - myEvents, mySubGroup);
267 
268     myEvents = myNewMessages;
269   }
270 
271   myEventType = 0;
272 
273   if (myNewMessages > 0)
274   {
275     for (unsigned short i = 0; i < myNewMessages; i++)
276     {
277       switch (u->EventPeek(i)->eventType())
278       {
279         case Licq::UserEvent::TypeFile:
280           myEventType = Licq::UserEvent::TypeFile;
281           break;
282         case Licq::UserEvent::TypeChat:
283           if (myEventType != Licq::UserEvent::TypeFile)
284             myEventType = Licq::UserEvent::TypeChat;
285           break;
286         case Licq::UserEvent::TypeUrl:
287           if (myEventType != Licq::UserEvent::TypeFile &&
288               myEventType != Licq::UserEvent::TypeChat)
289             myEventType = Licq::UserEvent::TypeUrl;
290           break;
291         case Licq::UserEvent::TypeContactList:
292           if(myEventType != Licq::UserEvent::TypeFile &&
293               myEventType != Licq::UserEvent::TypeChat &&
294               myEventType != Licq::UserEvent::TypeUrl)
295             myEventType = Licq::UserEvent::TypeContactList;
296         case Licq::UserEvent::TypeMessage:
297         default:
298           if (myEventType == 0)
299             myEventType = Licq::UserEvent::TypeMessage;
300           break;
301       }
302       if (u->EventPeek(i)->IsUrgent())
303         myUrgent = true;
304     }
305   }
306   Config::ContactList::FlashMode flash = Config::ContactList::instance()->flash();
307   bool shouldFlash = ((myNewMessages > 0 &&  flash == Config::ContactList::FlashAll) ||
308       (myUrgent && flash == Config::ContactList::FlashUrgent));
309 
310   if (shouldFlash != myFlash)
311   {
312     myFlash = shouldFlash;
313 
314     if (myFlash)
315     {
316       myFlashCounter = false;
317       startAnimation();
318     }
319   }
320 }
321 
updateExtendedStatus()322 void ContactUserData::updateExtendedStatus()
323 {
324   // Make a bitmask of everything the delegate needs to display extended icons
325   myExtendedStatus = 0;
326 
327   if (myStatusInvisible)
328     myExtendedStatus |= ContactListModel::InvisibleStatus;
329 
330   if (myStatusTyping)
331     myExtendedStatus |= ContactListModel::TypingStatus;
332 
333   if (myPhoneFollowMeStatus == Licq::IcqPluginActive)
334     myExtendedStatus |= ContactListModel::PhoneFollowMeActiveStatus;
335   else if (myPhoneFollowMeStatus == Licq::IcqPluginBusy)
336     myExtendedStatus |= ContactListModel::PhoneFollowMeBusyStatus;
337 
338   if (myIcqPhoneStatus == Licq::IcqPluginActive)
339     myExtendedStatus |= ContactListModel::IcqPhoneActiveStatus;
340   else if (myIcqPhoneStatus == Licq::IcqPluginBusy)
341     myExtendedStatus |= ContactListModel::IcqPhoneBusyStatus;
342 
343   if (mySharedFilesStatus == Licq::IcqPluginActive)
344     myExtendedStatus |= ContactListModel::SharedFilesStatus;
345 
346   if (myCustomAR)
347     myExtendedStatus |= ContactListModel::CustomArStatus;
348 
349   if (mySecure)
350     myExtendedStatus |= ContactListModel::SecureStatus;
351 
352   if (myBirthday)
353     myExtendedStatus |= ContactListModel::BirthdayStatus;
354 
355   if (myPhone)
356     myExtendedStatus |= ContactListModel::PhoneStatus;
357 
358   if (myCellular)
359     myExtendedStatus |= ContactListModel::CellularStatus;
360 
361   if (myGPGKey)
362     myExtendedStatus |= ContactListModel::GpgKeyStatus;
363 
364   if (myGPGKeyEnabled)
365     myExtendedStatus |= ContactListModel::GpgKeyEnabledStatus;
366 
367   if (myInIgnoreList)
368     myExtendedStatus |= ContactListModel::IgnoreStatus;
369 
370   if (myInOnlineNotify)
371     myExtendedStatus |= ContactListModel::OnlineNotifyStatus;
372 
373   if (myNotInList)
374     myExtendedStatus |= ContactListModel::NotInListStatus;
375 
376   if (myInInvisibleList)
377     myExtendedStatus |= ContactListModel::InvisibleListStatus;
378 
379   if (myInVisibleList)
380     myExtendedStatus |= ContactListModel::VisibleListStatus;
381 
382   if (myNewUser)
383     myExtendedStatus |= ContactListModel::NewUserStatus;
384 
385   if (myAwaitingAuth)
386     myExtendedStatus |= ContactListModel::AwaitingAuthStatus;
387 }
388 
updateSorting()389 void ContactUserData::updateSorting()
390 {
391   // Set status sort order
392   int sort = 9;
393   if (myStatus & User::OccupiedStatus)
394     sort = 1;
395   else if (myStatus & User::DoNotDisturbStatus)
396     sort = 2;
397   else if (myStatus & User::AwayStatus)
398     sort = 3;
399   else if (myStatus & User::NotAvailableStatus)
400     sort = 4;
401   else if (myStatus == User::OfflineStatus)
402     sort = 5;
403   else
404     sort = 0;
405 
406   // Set sorting
407   mySortKey = "";
408   switch (Config::ContactList::instance()->sortByStatus())
409   {
410     case 0:  // no sorting
411       break;
412     case 1:  // sort by status
413       mySortKey.sprintf("%1x", sort);
414       break;
415     case 2:  // sort by status and last event
416       mySortKey.sprintf("%1x%016lx", sort, ULONG_MAX - myTouched);
417       break;
418     case 3:  // sort by status and number of new messages
419       mySortKey.sprintf("%1x%016lx", sort, ULONG_MAX - myNewMessages);
420       break;
421   }
422   mySortKey += myText[0];
423 }
424 
updateText(const Licq::User * licqUser)425 bool ContactUserData::updateText(const Licq::User* licqUser)
426 {
427   bool hasChanged = false;
428 
429   myAlias = QString::fromUtf8(licqUser->getAlias().c_str());
430 
431   for (int i = 0; i < Config::ContactList::instance()->columnCount(); i++)
432   {
433     QString format = Config::ContactList::instance()->columnFormat(i);
434     format.replace("%a", "@_USER_ALIAS_@");
435     QString newStr = QString::fromLocal8Bit(licqUser->usprintf(format.toLocal8Bit().constData()).c_str());
436     newStr.replace("@_USER_ALIAS_@", myAlias);
437 
438     if (newStr != myText[i])
439     {
440       myText[i] = newStr;
441       hasChanged = true;
442     }
443   }
444   return hasChanged;
445 }
446 
configUpdated()447 void ContactUserData::configUpdated()
448 {
449   bool oldVisibility = myVisibility;
450 
451   {
452     Licq::UserReadGuard u(myUserId);
453     if (!u.isLocked())
454       return;
455 
456     updateText(*u);
457     updateSorting();
458     updateVisibility();
459   }
460 
461   emit dataChanged(this);
462 
463   // Update groups
464   if (myVisibility != oldVisibility)
465     foreach (ContactUser* user, myUserInstances)
466       user->group()->updateVisibility(myVisibility, mySubGroup);
467 }
468 
updateVisibility()469 void ContactUserData::updateVisibility()
470 {
471   myVisibility = false;
472 
473   // Only hide contacts who are offline
474   if (myStatus != User::OfflineStatus)
475     myVisibility = true;
476 
477   // Don't hide contacts with unread events
478   if (myEvents > 0)
479     myVisibility = true;
480 
481   // ... or the contact is in online notify list and option "Always show online notify users" is active
482   if (Config::ContactList::instance()->alwaysShowONU() &&
483       ((myExtendedStatus & ContactListModel::OnlineNotifyStatus) != 0))
484     myVisibility = true;
485 
486   // ... or the contact is not added to the list
487   if ((myExtendedStatus & ContactListModel::NotInListStatus) != 0)
488     myVisibility = true;
489 }
490 
setData(const QVariant & value,int role)491 bool ContactUserData::setData(const QVariant& value, int role)
492 {
493   if (role != ContactListModel::NameRole || !value.isValid())
494     return false;
495 
496   if (value.toString() == myAlias)
497     return true;
498 
499   {
500     Licq::UserWriteGuard u(myUserId);
501     if (!u.isLocked())
502       return false;
503 
504     myAlias = value.toString();
505     u->SetKeepAliasOnUpdate(true);
506     u->setAlias(myAlias.toUtf8().data());
507 
508     Licq::gPluginManager.pushPluginSignal(new Licq::PluginSignal(
509         Licq::PluginSignal::SignalUser, Licq::PluginSignal::UserBasic, myUserId));
510   }
511 
512   return true;
513 }
514 
refresh()515 void ContactUserData::refresh()
516 {
517   // Here we update any content that may be dynamic, for example timestamps
518 
519   bool birthday;
520   bool hasChanged;
521   {
522     Licq::UserReadGuard u(myUserId);
523     if (!u.isLocked())
524       return;
525 
526     // Check if birthday icon should be updated
527     birthday = (u->Birthday() == 0);
528     hasChanged = updateText(*u);
529   }
530 
531   if (birthday != myBirthday)
532   {
533     myBirthday = birthday;
534     hasChanged = true;
535     if (myBirthday)
536       myExtendedStatus |= ContactListModel::BirthdayStatus;
537     else
538       myExtendedStatus &= ~ContactListModel::BirthdayStatus;
539   }
540 
541   // To reduce performance impact on refreshes, keep track on
542   // whether anything has changed so we don't force unnecessary updates
543   if (hasChanged)
544   {
545     updateSorting();
546     emit dataChanged(this);
547   }
548 }
549 
startAnimation()550 void ContactUserData::startAnimation()
551 {
552   // Start common timer if not already running
553   if (!myAnimateTimer->isActive())
554     myAnimateTimer->start();
555 
556   // Attach to signal if we are not already animating something else
557   if (!myAnimating)
558   {
559     myAnimatorCount++;
560     connect(myAnimateTimer, SIGNAL(timeout()), SLOT(animate()));
561     myAnimating = true;
562   }
563 }
564 
stopAnimation()565 void ContactUserData::stopAnimation()
566 {
567   // Disconnect from timer and keep track of usage
568   disconnect(myAnimateTimer, SIGNAL(timeout()), this, SLOT(animate()));
569   myAnimatorCount--;
570 
571   // Stop animation timer if noone is using it anymore
572   if (myAnimatorCount == 0)
573     myAnimateTimer->stop();
574 
575   myAnimating = false;
576 }
577 
animate()578 void ContactUserData::animate()
579 {
580   // Animation for incoming event
581   if (myFlash)
582     myFlashCounter = !myFlashCounter;
583 
584   // Animation for going online
585   if (myOnlCounter > 0)
586     myOnlCounter--;
587 
588   // Animation for auto response read
589   if (myCarCounter > 0)
590     myCarCounter--;
591 
592   // Release timer if this was last animation
593   if (!myFlash && myOnlCounter == 0 && myCarCounter == 0)
594     stopAnimation();
595 
596   // data() will check the counter value to determine which icon to show so nothing to do here except triggering an update
597   emit dataChanged(this);
598 }
599 
addGroup(ContactUser * user)600 void ContactUserData::addGroup(ContactUser* user)
601 {
602   myUserInstances.append(user);
603 }
604 
removeGroup(ContactUser * user)605 void ContactUserData::removeGroup(ContactUser* user)
606 {
607   myUserInstances.removeAll(user);
608 }
609 
data(int column,int role) const610 QVariant ContactUserData::data(int column, int role) const
611 {
612   switch (role)
613   {
614     case Qt::DisplayRole:
615       if (column >= 0 && column < MAX_COLUMNCOUNT)
616         return myText[column];
617       break;
618 
619     case ContactListModel::NameRole:
620       return myAlias;
621 
622     case Qt::ToolTipRole:
623       return tooltip();
624 
625     case ContactListModel::UserIdRole:
626       return QVariant::fromValue(myUserId);
627 
628     case ContactListModel::AccountIdRole:
629       return myUserId.accountId().c_str();
630 
631     case ContactListModel::PpidRole:
632       return static_cast<unsigned int>(myUserId.protocolId());
633 
634     case ContactListModel::ItemTypeRole:
635       return ContactListModel::UserItem;
636 
637     case ContactListModel::SortPrefixRole:
638       // Primary sorting by sub group, make room for the seprator bars between each sub group
639       return 2 * mySubGroup + 1;
640 
641     case ContactListModel::SortRole:
642       return mySortKey;
643 
644     case ContactListModel::UnreadEventsRole:
645       return myEvents;
646 
647     case ContactListModel::SubGroupRole:
648       return mySubGroup;
649 
650     case ContactListModel::StatusRole:
651       return myStatus;
652 
653     case ContactListModel::ExtendedStatusRole:
654       return myExtendedStatus;
655 
656     case ContactListModel::UserIconRole:
657       if (myUserIcon != NULL)
658         return *myUserIcon;
659       break;
660 
661     case ContactListModel::EventTypeRole:
662       return myEventType;
663 
664     case ContactListModel::CarAnimationRole:
665       if (myCarCounter > 0)
666         return myCarCounter & 1;
667       break;
668 
669     case ContactListModel::OnlineAnimationRole:
670       if (myOnlCounter > 0)
671         return myOnlCounter & 1;
672       break;
673 
674     case ContactListModel::EventAnimationRole:
675       if (myFlash)
676         return myFlashCounter;
677       else if (myNewMessages > 0)
678         // No flashing but we have unread events so show a static event icon
679         return 1;
680       break;
681 
682     case ContactListModel::VisibilityRole:
683       return myVisibility;
684   }
685 
686   return QVariant();
687 }
688 
tooltip() const689 QString ContactUserData::tooltip() const
690 {
691   Licq::UserReadGuard u(myUserId);
692   if (!u.isLocked())
693     return "";
694 
695   Config::ContactList* config = Config::ContactList::instance();
696 
697   QString s = "<nobr>";
698   if (config->popupPicture() && u->GetPicturePresent())
699   {
700     QString file = QString::fromLocal8Bit(u->pictureFileName().c_str());
701     QImage picture = QImage(file);
702     if (!picture.isNull())
703       s += QString("<center><img src=\"%1\"></center>").arg(file);
704   }
705 
706   s += User::statusToString(myStatus).c_str();
707 
708   if (config->popupAlias() && !u->getAlias().empty())
709     s += "<br>" + QString::fromUtf8(u->getAlias().c_str());
710 
711   if (config->popupName())
712   {
713     string fullName = u->getFullName();
714     if (!fullName.empty())
715       s += "<br>" + QString::fromUtf8(fullName.c_str());
716   }
717 
718   if (myBirthday)
719     s += "<br><b>" + tr("Birthday Today!") + "</b>";
720 
721   if (myStatus != User::OfflineStatus)
722   {
723     if (myStatusTyping)
724       s += "<br>" + tr("Typing a message");
725     if (myPhoneFollowMeStatus == Licq::IcqPluginActive)
726       s += "<br>" + tr("Phone &quot;Follow Me&quot;: Available");
727     else if (myPhoneFollowMeStatus == Licq::IcqPluginBusy)
728       s += "<br>" + tr("Phone &quot;Follow Me&quot;: Busy");
729 
730     if (myIcqPhoneStatus == Licq::IcqPluginActive)
731       s += "<br>" + tr("ICQphone: Available");
732     else if (myIcqPhoneStatus == Licq::IcqPluginBusy)
733       s += "<br>" + tr("ICQphone: Busy");
734 
735     if (mySharedFilesStatus == Licq::IcqPluginActive)
736       s += "<br>" + tr("File Server: Enabled");
737   }
738 
739   if (mySecure)
740     s += "<br>" + tr("Secure connection");
741 
742   if (myCustomAR)
743     s += "<br>" + tr("Custom Auto Response");
744 
745   if (config->popupAuth() && u->GetAwaitingAuth())
746     s += "<br>" + tr("Awaiting authorization");
747 
748   if (u->isOnline() && !u->clientInfo().empty())
749     s += "<br>" + QString::fromUtf8(u->clientInfo().c_str());
750 
751   if (!u->autoResponse().empty() && myStatus & User::MessageStatuses)
752     s += "<br><u>" + tr("Auto Response:") + "</u><br>&nbsp;&nbsp;&nbsp;" +
753       QString::fromUtf8(u->autoResponse().c_str()).trimmed()
754       .replace("\n", "<br>&nbsp;&nbsp;&nbsp;");
755 
756   if (config->popupEmail())
757   {
758     string email = u->getEmail();
759     if (!email.empty())
760       s += "<br>" + tr("E: ") + QString::fromUtf8(email.c_str());
761   }
762 
763   if (config->popupPhone() && myPhone)
764     s += "<br>" + tr("P: ") + QString::fromUtf8(u->getUserInfoString("PhoneNumber").c_str());
765 
766   if (config->popupCellular() && myCellular)
767     s += "<br>" + tr("C: ") + QString::fromUtf8(u->getCellularNumber().c_str());
768 
769   if (config->popupFax())
770   {
771     string faxNumber = u->getUserInfoString("FaxNumber");
772     if (!faxNumber.empty())
773       s += "<br>" + tr("F: ") + QString::fromUtf8(faxNumber.c_str());
774   }
775 
776   if (config->popupIP() && (u->Ip() || u->IntIp()))
777   {
778     char buf[32];
779     Licq::ip_ntoa(u->Ip(), buf);
780     s += "<br>" + tr("Ip: ") + QString::fromLatin1(buf);
781     if (u->IntIp() != 0 && u->IntIp() != u->Ip())
782     {
783       Licq::ip_ntoa(u->IntIp(), buf);
784       s += " / " + QString::fromLatin1(buf);
785     }
786   }
787 
788   if (config->popupLastOnline() && u->LastOnline() > 0)
789   {
790     QDateTime t;
791     t.setTime_t(u->LastOnline());
792     s += "<br>" + tr("O: ") + t.toString();
793   }
794 
795   if (config->popupOnlineSince() && u->isOnline() && u->OnlineSince() > 0 && u->OnlineSince() <= time(0))
796     s += "<br>" + tr("Logged In: ") + User::RelativeStrTime(u->OnlineSince()).c_str();
797 
798   if (config->popupAwayTime() && (myStatus & User::AwayStatuses) && u->awaySince())
799     s += "<br>" + tr("Away: ") + User::RelativeStrTime(u->awaySince()).c_str();
800 
801   if (config->popupIdleTime() && u->IdleSince())
802     s += "<br>" + tr("Idle: ") + User::RelativeStrTime(u->IdleSince()).c_str();
803 
804   if (config->popupLocalTime())
805     s += "<br>" + tr("Local time: ") + u->usprintf("%F").c_str();
806 
807   if (config->popupID())
808     s += "<br>" + tr("ID: ") + u->usprintf("%u").c_str();
809 
810   s += "</nobr>";
811 
812   return s;
813 }
814