Add /raw as an alias for /quote
[quassel.git] / src / client / networkmodel.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2020 by the Quassel Project                        *
3  *   devel@quassel-irc.org                                                 *
4  *                                                                         *
5  *   This program 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) version 3.                                           *
9  *                                                                         *
10  *   This program 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 this program; if not, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
19  ***************************************************************************/
20
21 #include "networkmodel.h"
22
23 #include <algorithm>
24 #include <utility>
25
26 #include <QAbstractItemView>
27 #include <QMimeData>
28
29 #include "buffermodel.h"
30 #include "buffersettings.h"
31 #include "buffersyncer.h"
32 #include "client.h"
33 #include "clientignorelistmanager.h"
34 #include "clientsettings.h"
35 #include "ircchannel.h"
36 #include "network.h"
37 #include "signalproxy.h"
38
39 /*****************************************
40  *  Network Items
41  *****************************************/
42 NetworkItem::NetworkItem(const NetworkId& netid, AbstractTreeItem* parent)
43     : PropertyMapItem(parent)
44     , _networkId(netid)
45     , _statusBufferItem(nullptr)
46 {
47     // DO NOT EMIT dataChanged() DIRECTLY IN NetworkItem
48     // use networkDataChanged() instead. Otherwise you will end up in a infinite loop
49     // as we "sync" the dataChanged() signals of NetworkItem and StatusBufferItem
50     setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
51     connect(this, &NetworkItem::networkDataChanged, this, &NetworkItem::dataChanged);
52     connect(this, &NetworkItem::beginRemoveChilds, this, &NetworkItem::onBeginRemoveChilds);
53 }
54
55 QStringList NetworkItem::propertyOrder() const
56 {
57     static QStringList order{"networkName", "currentServer", "nickCount"};
58     return order;
59 }
60
61 QVariant NetworkItem::data(int column, int role) const
62 {
63     switch (role) {
64     case NetworkModel::BufferIdRole:
65     case NetworkModel::BufferInfoRole:
66     case NetworkModel::BufferTypeRole:
67     case NetworkModel::BufferActivityRole:
68         if (_statusBufferItem)
69             return _statusBufferItem->data(column, role);
70         else
71             return QVariant();
72     case NetworkModel::NetworkIdRole:
73         return QVariant::fromValue(_networkId);
74     case NetworkModel::ItemTypeRole:
75         return NetworkModel::NetworkItemType;
76     case NetworkModel::ItemActiveRole:
77         return isActive();
78     default:
79         return PropertyMapItem::data(column, role);
80     }
81 }
82
83 QString NetworkItem::escapeHTML(const QString& string, bool useNonbreakingSpaces)
84 {
85     // QString.replace() doesn't guarantee the source string will remain constant.
86     // Use a local variable to avoid compiler errors.
87     QString formattedString = string.toHtmlEscaped();
88     return (useNonbreakingSpaces ? formattedString.replace(" ", "&nbsp;") : formattedString);
89 }
90
91 // FIXME shouldn't we check the bufferItemCache here?
92 BufferItem* NetworkItem::findBufferItem(BufferId bufferId)
93 {
94     BufferItem* bufferItem = nullptr;
95
96     for (int i = 0; i < childCount(); i++) {
97         bufferItem = qobject_cast<BufferItem*>(child(i));
98         if (!bufferItem)
99             continue;
100         if (bufferItem->bufferId() == bufferId)
101             return bufferItem;
102     }
103     return nullptr;
104 }
105
106 BufferItem* NetworkItem::bufferItem(const BufferInfo& bufferInfo)
107 {
108     BufferItem* bufferItem = findBufferItem(bufferInfo);
109     if (bufferItem)
110         return bufferItem;
111
112     switch (bufferInfo.type()) {
113     case BufferInfo::StatusBuffer:
114         _statusBufferItem = new StatusBufferItem(bufferInfo, this);
115         bufferItem = _statusBufferItem;
116         disconnect(this, &NetworkItem::networkDataChanged, this, &NetworkItem::dataChanged);
117         connect(this, &NetworkItem::networkDataChanged, bufferItem, &BufferItem::dataChanged);
118         connect(bufferItem, &BufferItem::dataChanged, this, &NetworkItem::dataChanged);
119         break;
120     case BufferInfo::ChannelBuffer:
121         bufferItem = new ChannelBufferItem(bufferInfo, this);
122         break;
123     case BufferInfo::QueryBuffer:
124         bufferItem = new QueryBufferItem(bufferInfo, this);
125         break;
126     default:
127         bufferItem = new BufferItem(bufferInfo, this);
128     }
129
130     newChild(bufferItem);
131
132     // postprocess... this is necessary because Qt doesn't seem to like adding children which already have children on their own
133     switch (bufferInfo.type()) {
134     case BufferInfo::ChannelBuffer: {
135         auto* channelBufferItem = static_cast<ChannelBufferItem*>(bufferItem);
136         if (_network) {
137             IrcChannel* ircChannel = _network->ircChannel(bufferInfo.bufferName());
138             if (ircChannel)
139                 channelBufferItem->attachIrcChannel(ircChannel);
140         }
141     } break;
142     default:
143         break;
144     }
145
146     BufferSyncer* bufferSyncer = Client::bufferSyncer();
147     if (bufferSyncer) {
148         bufferItem->addActivity(bufferSyncer->activity(bufferItem->bufferId()), bufferSyncer->highlightCount(bufferItem->bufferId()) > 0);
149     }
150
151     return bufferItem;
152 }
153
154 void NetworkItem::attachNetwork(Network* network)
155 {
156     if (!network)
157         return;
158
159     _network = network;
160
161     connect(network, &Network::networkNameSet, this, &NetworkItem::setNetworkName);
162     connect(network, &Network::currentServerSet, this, &NetworkItem::setCurrentServer);
163     connect(network, &Network::ircChannelAdded, this, &NetworkItem::attachIrcChannel);
164     connect(network, &Network::ircUserAdded, this, &NetworkItem::attachIrcUser);
165     connect(network, &Network::connectedSet, this, [this]() { emit networkDataChanged(); });
166     connect(network, &QObject::destroyed, this, &NetworkItem::onNetworkDestroyed);
167
168     emit networkDataChanged();
169 }
170
171 void NetworkItem::attachIrcChannel(IrcChannel* ircChannel)
172 {
173     ChannelBufferItem* channelItem;
174     for (int i = 0; i < childCount(); i++) {
175         channelItem = qobject_cast<ChannelBufferItem*>(child(i));
176         if (!channelItem)
177             continue;
178
179         if (channelItem->bufferName().toLower() == ircChannel->name().toLower()) {
180             channelItem->attachIrcChannel(ircChannel);
181             return;
182         }
183     }
184 }
185
186 void NetworkItem::attachIrcUser(IrcUser* ircUser)
187 {
188     QueryBufferItem* queryItem = nullptr;
189     for (int i = 0; i < childCount(); i++) {
190         queryItem = qobject_cast<QueryBufferItem*>(child(i));
191         if (!queryItem)
192             continue;
193
194         if (queryItem->bufferName().toLower() == ircUser->nick().toLower()) {
195             queryItem->setIrcUser(ircUser);
196             break;
197         }
198     }
199 }
200
201 void NetworkItem::setNetworkName(const QString& networkName)
202 {
203     Q_UNUSED(networkName);
204     emit networkDataChanged(0);
205 }
206
207 void NetworkItem::setCurrentServer(const QString& serverName)
208 {
209     Q_UNUSED(serverName);
210     emit networkDataChanged(1);
211 }
212
213 QString NetworkItem::toolTip(int column) const
214 {
215     Q_UNUSED(column);
216     QString strTooltip;
217     QTextStream tooltip(&strTooltip, QIODevice::WriteOnly);
218     tooltip << "<qt><style>.bold { font-weight: bold; } .italic { font-style: italic; }</style>";
219
220     // Function to add a row to the tooltip table
221     auto addRow = [&](const QString& key, const QString& value, bool condition) {
222         if (condition) {
223             tooltip << "<tr><td class='bold' align='right'>" << key << "</td><td>" << value << "</td></tr>";
224         }
225     };
226
227     tooltip << "<p class='bold' align='center'>" << NetworkItem::escapeHTML(networkName(), true) << "</p>";
228     if (isActive()) {
229         tooltip << "<table cellspacing='5' cellpadding='0'>";
230         addRow(tr("Server"), NetworkItem::escapeHTML(currentServer(), true), !currentServer().isEmpty());
231         addRow(tr("Users"), QString::number(nickCount()), true);
232         if (_network)
233             addRow(tr("Lag"), NetworkItem::escapeHTML(tr("%1 msecs").arg(_network->latency()), true), true);
234
235         tooltip << "</table>";
236     }
237     else {
238         tooltip << "<p class='italic' align='center'>" << tr("Not connected") << "</p>";
239     }
240     tooltip << "</qt>";
241     return strTooltip;
242 }
243
244 void NetworkItem::onBeginRemoveChilds(int start, int end)
245 {
246     for (int i = start; i <= end; i++) {
247         auto* statusBufferItem = qobject_cast<StatusBufferItem*>(child(i));
248         if (statusBufferItem) {
249             _statusBufferItem = nullptr;
250             break;
251         }
252     }
253 }
254
255 void NetworkItem::onNetworkDestroyed()
256 {
257     _network = nullptr;
258     emit networkDataChanged();
259     removeAllChilds();
260 }
261
262 /*****************************************
263  *  Fancy Buffer Items
264  *****************************************/
265 BufferItem::BufferItem(BufferInfo bufferInfo, AbstractTreeItem* parent)
266     : PropertyMapItem(parent)
267     , _bufferInfo(std::move(bufferInfo))
268     , _activity(BufferInfo::NoActivity)
269 {
270     setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
271 }
272
273 QStringList BufferItem::propertyOrder() const
274 {
275     static QStringList order{"bufferName", "topic", "nickCount"};
276     return order;
277 }
278
279 void BufferItem::setActivityLevel(BufferInfo::ActivityLevel level)
280 {
281     if (_activity != level) {
282         _activity = level;
283         emit dataChanged();
284     }
285 }
286
287 void BufferItem::clearActivityLevel()
288 {
289     if (Client::isCoreFeatureEnabled(Quassel::Feature::BufferActivitySync)) {
290         // If the core handles activity sync, clear only the highlight flag
291         _activity &= ~BufferInfo::Highlight;
292     }
293     else {
294         _activity = BufferInfo::NoActivity;
295     }
296     _firstUnreadMsgId = MsgId();
297
298     // FIXME remove with core proto v11
299     if (!Client::isCoreFeatureEnabled(Quassel::Feature::SynchronizedMarkerLine)) {
300         _markerLineMsgId = _lastSeenMsgId;
301     }
302
303     emit dataChanged();
304 }
305
306 void BufferItem::updateActivityLevel(const Message& msg)
307 {
308     // If the core handles activity, and this message is not a highlight, ignore this
309     if (Client::isCoreFeatureEnabled(Quassel::Feature::BufferActivitySync) && !msg.flags().testFlag(Message::Highlight)) {
310         return;
311     }
312
313     if (isCurrentBuffer()) {
314         return;
315     }
316
317     if (msg.flags() & Message::Self)  // don't update activity for our own messages
318         return;
319
320     if (Client::ignoreListManager() && Client::ignoreListManager()->match(msg, qobject_cast<NetworkItem*>(parent())->networkName()))
321         return;
322
323     if (msg.msgId() <= lastSeenMsgId())
324         return;
325
326     bool stateChanged = false;
327     if (!firstUnreadMsgId().isValid() || msg.msgId() < firstUnreadMsgId()) {
328         stateChanged = true;
329         _firstUnreadMsgId = msg.msgId();
330     }
331
332     Message::Types type;
333     // If the core handles activities, ignore types
334     if (Client::isCoreFeatureEnabled(Quassel::Feature::BufferActivitySync)) {
335         type = Message::Types();
336     }
337     else {
338         type = msg.type();
339     }
340
341     if (addActivity(type, msg.flags().testFlag(Message::Highlight)) || stateChanged) {
342         emit dataChanged();
343     }
344 }
345
346 void BufferItem::setActivity(Message::Types type, bool highlight)
347 {
348     BufferInfo::ActivityLevel oldLevel = activityLevel();
349
350     _activity &= BufferInfo::Highlight;
351     addActivity(type, highlight);
352
353     if (_activity != oldLevel) {
354         emit dataChanged();
355     }
356 }
357
358 bool BufferItem::addActivity(Message::Types type, bool highlight)
359 {
360     auto oldActivity = activityLevel();
361
362     if (type != Message::Types())
363         _activity |= BufferInfo::OtherActivity;
364
365     if (type.testFlag(Message::Plain) || type.testFlag(Message::Notice) || type.testFlag(Message::Action))
366         _activity |= BufferInfo::NewMessage;
367
368     if (highlight)
369         _activity |= BufferInfo::Highlight;
370
371     return oldActivity != _activity;
372 }
373
374 QVariant BufferItem::data(int column, int role) const
375 {
376     switch (role) {
377     case NetworkModel::ItemTypeRole:
378         return NetworkModel::BufferItemType;
379     case NetworkModel::BufferIdRole:
380         return QVariant::fromValue(bufferInfo().bufferId());
381     case NetworkModel::NetworkIdRole:
382         return QVariant::fromValue(bufferInfo().networkId());
383     case NetworkModel::BufferInfoRole:
384         return QVariant::fromValue(bufferInfo());
385     case NetworkModel::BufferTypeRole:
386         return int(bufferType());
387     case NetworkModel::ItemActiveRole:
388         return isActive();
389     case NetworkModel::BufferActivityRole:
390         return (int)activityLevel();
391     case NetworkModel::BufferFirstUnreadMsgIdRole:
392         return QVariant::fromValue(firstUnreadMsgId());
393     case NetworkModel::MarkerLineMsgIdRole:
394         return QVariant::fromValue(markerLineMsgId());
395     default:
396         return PropertyMapItem::data(column, role);
397     }
398 }
399
400 bool BufferItem::setData(int column, const QVariant& value, int role)
401 {
402     switch (role) {
403     case NetworkModel::BufferActivityRole:
404         setActivityLevel((BufferInfo::ActivityLevel)value.toInt());
405         return true;
406     default:
407         return PropertyMapItem::setData(column, value, role);
408     }
409 }
410
411 void BufferItem::setBufferName(const QString& name)
412 {
413     _bufferInfo = BufferInfo(_bufferInfo.bufferId(), _bufferInfo.networkId(), _bufferInfo.type(), _bufferInfo.groupId(), name);
414     emit dataChanged(0);
415 }
416
417 void BufferItem::setLastSeenMsgId(MsgId msgId)
418 {
419     _lastSeenMsgId = msgId;
420
421     // FIXME remove with core protocol v11
422     if (!Client::isCoreFeatureEnabled(Quassel::Feature::SynchronizedMarkerLine)) {
423         if (!isCurrentBuffer())
424             _markerLineMsgId = msgId;
425     }
426
427     setActivityLevel(BufferInfo::NoActivity);
428 }
429
430 void BufferItem::setMarkerLineMsgId(MsgId msgId)
431 {
432     _markerLineMsgId = msgId;
433     emit dataChanged();
434 }
435
436 bool BufferItem::isCurrentBuffer() const
437 {
438     return _bufferInfo.bufferId() == Client::bufferModel()->currentIndex().data(NetworkModel::BufferIdRole).value<BufferId>();
439 }
440
441 QString BufferItem::toolTip(int column) const
442 {
443     Q_UNUSED(column);
444     return tr("<p> %1 - %2 </p>").arg(bufferInfo().bufferId().toInt()).arg(bufferName());
445 }
446
447 /*****************************************
448  *  StatusBufferItem
449  *****************************************/
450 StatusBufferItem::StatusBufferItem(const BufferInfo& bufferInfo, NetworkItem* parent)
451     : BufferItem(bufferInfo, parent)
452 {}
453
454 QString StatusBufferItem::toolTip(int column) const
455 {
456     auto* networkItem = qobject_cast<NetworkItem*>(parent());
457     if (networkItem)
458         return networkItem->toolTip(column);
459     else
460         return QString();
461 }
462
463 /*****************************************
464  *  QueryBufferItem
465  *****************************************/
466 QueryBufferItem::QueryBufferItem(const BufferInfo& bufferInfo, NetworkItem* parent)
467     : BufferItem(bufferInfo, parent)
468     , _ircUser(nullptr)
469 {
470     setFlags(flags() | Qt::ItemIsDropEnabled | Qt::ItemIsEditable);
471
472     const Network* net = Client::network(bufferInfo.networkId());
473     if (!net)
474         return;
475
476     IrcUser* ircUser = net->ircUser(bufferInfo.bufferName());
477     setIrcUser(ircUser);
478 }
479
480 QVariant QueryBufferItem::data(int column, int role) const
481 {
482     switch (role) {
483     case Qt::EditRole:
484         return BufferItem::data(column, Qt::DisplayRole);
485     case NetworkModel::IrcUserRole:
486         return QVariant::fromValue(_ircUser);
487     case NetworkModel::UserAwayRole:
488         return (bool)_ircUser ? _ircUser->isAway() : false;
489     default:
490         return BufferItem::data(column, role);
491     }
492 }
493
494 bool QueryBufferItem::setData(int column, const QVariant& value, int role)
495 {
496     if (column != 0)
497         return BufferItem::setData(column, value, role);
498
499     switch (role) {
500     case Qt::EditRole: {
501         QString newName = value.toString();
502
503         // Sanity check - buffer names must not contain newlines!
504         int nlpos = newName.indexOf('\n');
505         if (nlpos >= 0)
506             newName = newName.left(nlpos);
507
508         if (!newName.isEmpty()) {
509             Client::renameBuffer(bufferId(), newName);
510             return true;
511         }
512         else {
513             return false;
514         }
515     } break;
516     default:
517         return BufferItem::setData(column, value, role);
518     }
519 }
520
521 void QueryBufferItem::setBufferName(const QString& name)
522 {
523     BufferItem::setBufferName(name);
524     NetworkId netId = data(0, NetworkModel::NetworkIdRole).value<NetworkId>();
525     const Network* net = Client::network(netId);
526     if (net)
527         setIrcUser(net->ircUser(name));
528 }
529
530 QString QueryBufferItem::toolTip(int column) const
531 {
532     // pretty much code duplication of IrcUserItem::toolTip() but inheritance won't solve this...
533     Q_UNUSED(column);
534     QString strTooltip;
535     QTextStream tooltip(&strTooltip, QIODevice::WriteOnly);
536     tooltip << "<qt><style>.bold { font-weight: bold; } .italic { font-style: italic; }</style>";
537
538     // Keep track of whether or not information has been added
539     bool infoAdded = false;
540
541     // Use bufferName() for QueryBufferItem, nickName() for IrcUserItem
542     tooltip << "<p class='bold' align='center'>";
543     tooltip << tr("Query with %1").arg(NetworkItem::escapeHTML(bufferName(), true));
544     if (!_ircUser) {
545         // User seems to be offline, let the no information message be added below
546         tooltip << "</p>";
547     }
548     else {
549         // Function to add a row to the tooltip table
550         auto addRow = [&](const QString& key, const QString& value, bool condition) {
551             if (condition) {
552                 tooltip << "<tr><td class='bold' align='right'>" << key << "</td><td>" << value << "</td></tr>";
553                 infoAdded = true;
554             }
555         };
556
557         // User information is available
558         if (_ircUser->userModes() != "") {
559             // TODO Translate user Modes and add them to the table below and in IrcUserItem::toolTip
560             tooltip << " (" << _ircUser->userModes() << ")";
561         }
562         tooltip << "</p>";
563
564         tooltip << "<table cellspacing='5' cellpadding='0'>";
565         if (_ircUser->isAway()) {
566             QString awayMessageHTML = QString("<p class='italic'>%1</p>").arg(tr("Unknown"));
567
568             // If away message is known, replace with the escaped message.
569             if (!_ircUser->awayMessage().isEmpty()) {
570                 awayMessageHTML = NetworkItem::escapeHTML(_ircUser->awayMessage());
571             }
572             addRow(NetworkItem::escapeHTML(tr("Away message"), true), awayMessageHTML, true);
573         }
574         addRow(tr("Realname"), NetworkItem::escapeHTML(_ircUser->realName()), !_ircUser->realName().isEmpty());
575         // suserHost may return "<nick> is available for help", which should be translated.
576         // See https://www.alien.net.au/irc/irc2numerics.html
577         if (_ircUser->suserHost().endsWith("available for help")) {
578             addRow(NetworkItem::escapeHTML(tr("Help status"), true), NetworkItem::escapeHTML(tr("Available for help")), true);
579         }
580         else {
581             addRow(NetworkItem::escapeHTML(tr("Service status"), true),
582                    NetworkItem::escapeHTML(_ircUser->suserHost()),
583                    !_ircUser->suserHost().isEmpty());
584         }
585
586         // Keep track of whether or not the account information's been added.  Don't show it twice.
587         bool accountAdded = false;
588         if (!_ircUser->account().isEmpty()) {
589             // IRCv3 account-notify is supported by the core and IRC server.
590             // Assume logged out (seems to be more common)
591             QString accountHTML = QString("<p class='italic'>%1</p>").arg(tr("Not logged in"));
592
593             // If account is logged in, replace with the escaped account name.
594             if (_ircUser->account() != "*") {
595                 accountHTML = NetworkItem::escapeHTML(_ircUser->account());
596             }
597             addRow(NetworkItem::escapeHTML(tr("Account"), true), accountHTML, true);
598             // Mark the row as added
599             accountAdded = true;
600         }
601         // whoisServiceReply may return "<nick> is identified for this nick", which should be translated.
602         // See https://www.alien.net.au/irc/irc2numerics.html
603         if (_ircUser->whoisServiceReply().endsWith("identified for this nick")) {
604             addRow(NetworkItem::escapeHTML(tr("Account"), true), NetworkItem::escapeHTML(tr("Identified for this nick")), !accountAdded);
605             // Don't add the account row again if information's already added via account-notify
606             // Not used further down...
607             // accountAdded = true;
608         }
609         else {
610             addRow(NetworkItem::escapeHTML(tr("Service Reply"), true),
611                    NetworkItem::escapeHTML(_ircUser->whoisServiceReply()),
612                    !_ircUser->whoisServiceReply().isEmpty());
613         }
614         addRow(tr("Hostmask"),
615                NetworkItem::escapeHTML(_ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!") + 1)),
616                !(_ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!") + 1) == "@"));
617         // ircOperator may contain "is an" or "is a", which should be removed.
618         addRow(tr("Operator"),
619                NetworkItem::escapeHTML(_ircUser->ircOperator().replace("is an ", "").replace("is a ", "")),
620                !_ircUser->ircOperator().isEmpty());
621
622         if (_ircUser->idleTime().isValid()) {
623             QDateTime now = QDateTime::currentDateTime();
624             QDateTime idle = _ircUser->idleTime();
625             int idleTime = idle.secsTo(now);
626             addRow(NetworkItem::escapeHTML(tr("Idling since"), true), secondsToString(idleTime), true);
627         }
628
629         if (_ircUser->loginTime().isValid()) {
630             addRow(NetworkItem::escapeHTML(tr("Login time"), true), _ircUser->loginTime().toString(), true);
631         }
632
633         addRow(tr("Server"), NetworkItem::escapeHTML(_ircUser->server()), !_ircUser->server().isEmpty());
634         tooltip << "</table>";
635     }
636
637     // If no further information found, offer an explanatory message
638     if (!infoAdded)
639         tooltip << "<p class='italic' align='center'>" << tr("No information available") << "</p>";
640
641     tooltip << "</qt>";
642     return strTooltip;
643 }
644
645 void QueryBufferItem::setIrcUser(IrcUser* ircUser)
646 {
647     if (_ircUser == ircUser)
648         return;
649
650     if (_ircUser) {
651         disconnect(_ircUser, nullptr, this, nullptr);
652     }
653
654     if (ircUser) {
655         connect(ircUser, &IrcUser::destroyed, this, &QueryBufferItem::removeIrcUser);
656         connect(ircUser, &IrcUser::quited, this, &QueryBufferItem::removeIrcUser);
657         connect(ircUser, &IrcUser::awaySet, this, [this]() { emit dataChanged(); });
658         connect(ircUser, &IrcUser::encryptedSet, this, &BufferItem::setEncrypted);
659     }
660
661     _ircUser = ircUser;
662     emit dataChanged();
663 }
664
665 void QueryBufferItem::removeIrcUser()
666 {
667     if (_ircUser) {
668         // Disconnect the active IrcUser before removing it, otherwise it will fire removeIrcUser()
669         // a second time when the object's destroyed due to QueryBufferItem::setIrcUser() connecting
670         // SIGNAL destroyed(QObject*) to SLOT removeIrcUser().
671         // This fixes removing an active IrcUser if the user had quit then rejoined in a nonstandard
672         // manner (e.g. updateNickFromHost calling newIrcUser, triggered by an away-notify message).
673         disconnect(_ircUser, nullptr, this, nullptr);
674
675         // Clear IrcUser (only set to 0 if not already 0)
676         _ircUser = nullptr;
677
678         // Only emit dataChanged() if data actually changed.  This might serve as a small
679         // optimization, but it can be moved outside the if statement if other behavior depends on
680         // it always being called.
681         emit dataChanged();
682     }
683 }
684
685 /*****************************************
686  *  ChannelBufferItem
687  *****************************************/
688 ChannelBufferItem::ChannelBufferItem(const BufferInfo& bufferInfo, AbstractTreeItem* parent)
689     : BufferItem(bufferInfo, parent)
690     , _ircChannel(nullptr)
691 {
692     setFlags(flags() | Qt::ItemIsDropEnabled);
693 }
694
695 QVariant ChannelBufferItem::data(int column, int role) const
696 {
697     switch (role) {
698     case NetworkModel::IrcChannelRole:
699         return QVariant::fromValue(_ircChannel);
700     default:
701         return BufferItem::data(column, role);
702     }
703 }
704
705 QString ChannelBufferItem::toolTip(int column) const
706 {
707     Q_UNUSED(column);
708     QString strTooltip;
709     QTextStream tooltip(&strTooltip, QIODevice::WriteOnly);
710     tooltip << "<qt><style>.bold { font-weight: bold; } .italic { font-style: italic; }</style>";
711
712     // Function to add a row to the tooltip table
713     auto addRow = [&](const QString& key, const QString& value, bool condition) {
714         if (condition) {
715             tooltip << "<tr><td class='bold' align='right'>" << key << "</td><td>" << value << "</td></tr>";
716         }
717     };
718
719     tooltip << "<p class='bold' align='center'>";
720     tooltip << NetworkItem::escapeHTML(tr("Channel %1").arg(bufferName()), true) << "</p>";
721
722     if (isActive()) {
723         tooltip << "<table cellspacing='5' cellpadding='0'>";
724         addRow(tr("Users"), QString::number(nickCount()), true);
725
726         if (_ircChannel) {
727             QString channelMode = _ircChannel->channelModeString();  // channelModeString is compiled on the fly -> thus cache the result
728             if (!channelMode.isEmpty())
729                 addRow(tr("Mode"), channelMode, true);
730         }
731
732         ItemViewSettings s;
733         bool showTopic = s.displayTopicInTooltip();
734         if (showTopic) {
735             QString _topic = topic();
736             if (_topic != "") {
737                 _topic = stripFormatCodes(_topic);
738                 _topic = NetworkItem::escapeHTML(_topic);
739                 addRow(tr("Topic"), _topic, true);
740             }
741         }
742
743         tooltip << "</table>";
744     }
745     else {
746         tooltip << "<p class='italic' align='center'>" << tr("Not active, double-click to join") << "</p>";
747     }
748
749     tooltip << "</qt>";
750     return strTooltip;
751 }
752
753 void ChannelBufferItem::attachIrcChannel(IrcChannel* ircChannel)
754 {
755     if (_ircChannel) {
756         qWarning() << Q_FUNC_INFO << "IrcChannel already set; cleanup failed!?";
757         disconnect(_ircChannel, nullptr, this, nullptr);
758     }
759
760     _ircChannel = ircChannel;
761
762     connect(ircChannel, &QObject::destroyed, this, &ChannelBufferItem::ircChannelDestroyed);
763     connect(ircChannel, &IrcChannel::topicSet, this, &ChannelBufferItem::setTopic);
764     connect(ircChannel, &IrcChannel::encryptedSet, this, &ChannelBufferItem::setEncrypted);
765     connect(ircChannel, &IrcChannel::ircUsersJoined, this, &ChannelBufferItem::join);
766     connect(ircChannel, &IrcChannel::ircUserParted, this, &ChannelBufferItem::part);
767     connect(ircChannel, &IrcChannel::parted, this, &ChannelBufferItem::ircChannelParted);
768     connect(ircChannel, &IrcChannel::ircUserModesSet, this, &ChannelBufferItem::userModeChanged);
769     connect(ircChannel, &IrcChannel::ircUserModeAdded, this, &ChannelBufferItem::userModeChanged);
770     connect(ircChannel, &IrcChannel::ircUserModeRemoved, this, &ChannelBufferItem::userModeChanged);
771
772     if (!ircChannel->ircUsers().isEmpty())
773         join(ircChannel->ircUsers());
774
775     emit dataChanged();
776 }
777
778 QString ChannelBufferItem::nickChannelModes(const QString& nick) const
779 {
780     if (!_ircChannel) {
781         qDebug() << Q_FUNC_INFO << "IrcChannel not set, can't get user modes";
782         return QString();
783     }
784
785     return _ircChannel->userModes(nick);
786 }
787
788 void ChannelBufferItem::ircChannelParted()
789 {
790     Q_CHECK_PTR(_ircChannel);
791     disconnect(_ircChannel, nullptr, this, nullptr);
792     _ircChannel = nullptr;
793     emit dataChanged();
794     removeAllChilds();
795 }
796
797 void ChannelBufferItem::ircChannelDestroyed()
798 {
799     if (_ircChannel) {
800         _ircChannel = nullptr;
801         emit dataChanged();
802         removeAllChilds();
803     }
804 }
805
806 void ChannelBufferItem::join(const QList<IrcUser*>& ircUsers)
807 {
808     addUsersToCategory(ircUsers);
809     emit dataChanged(2);
810 }
811
812 UserCategoryItem* ChannelBufferItem::findCategoryItem(int categoryId)
813 {
814     UserCategoryItem* categoryItem = nullptr;
815
816     for (int i = 0; i < childCount(); i++) {
817         categoryItem = qobject_cast<UserCategoryItem*>(child(i));
818         if (!categoryItem)
819             continue;
820         if (categoryItem->categoryId() == categoryId)
821             return categoryItem;
822     }
823     return nullptr;
824 }
825
826 void ChannelBufferItem::addUserToCategory(IrcUser* ircUser)
827 {
828     addUsersToCategory(QList<IrcUser*>() << ircUser);
829 }
830
831 void ChannelBufferItem::addUsersToCategory(const QList<IrcUser*>& ircUsers)
832 {
833     Q_ASSERT(_ircChannel);
834
835     QHash<UserCategoryItem*, QList<IrcUser*>> categories;
836
837     int categoryId = -1;
838     UserCategoryItem* categoryItem = nullptr;
839
840     foreach (IrcUser* ircUser, ircUsers) {
841         categoryId = UserCategoryItem::categoryFromModes(_ircChannel->userModes(ircUser));
842         categoryItem = findCategoryItem(categoryId);
843         if (!categoryItem) {
844             categoryItem = new UserCategoryItem(categoryId, this);
845             categories[categoryItem] = QList<IrcUser*>();
846             newChild(categoryItem);
847         }
848         categories[categoryItem] << ircUser;
849     }
850
851     QHash<UserCategoryItem*, QList<IrcUser*>>::const_iterator catIter = categories.constBegin();
852     while (catIter != categories.constEnd()) {
853         catIter.key()->addUsers(catIter.value());
854         ++catIter;
855     }
856 }
857
858 void ChannelBufferItem::part(IrcUser* ircUser)
859 {
860     if (!ircUser) {
861         qWarning() << bufferName() << "ChannelBufferItem::part(): unknown User" << ircUser;
862         return;
863     }
864
865     disconnect(ircUser, nullptr, this, nullptr);
866     removeUserFromCategory(ircUser);
867     emit dataChanged(2);
868 }
869
870 void ChannelBufferItem::removeUserFromCategory(IrcUser* ircUser)
871 {
872     if (!_ircChannel) {
873         // If we parted the channel there might still be some ircUsers connected.
874         // in that case we just ignore the call
875         Q_ASSERT(childCount() == 0);
876         return;
877     }
878
879     UserCategoryItem* categoryItem = nullptr;
880     for (int i = 0; i < childCount(); i++) {
881         categoryItem = qobject_cast<UserCategoryItem*>(child(i));
882         if (categoryItem->removeUser(ircUser)) {
883             if (categoryItem->childCount() == 0)
884                 removeChild(i);
885             break;
886         }
887     }
888 }
889
890 void ChannelBufferItem::userModeChanged(IrcUser* ircUser)
891 {
892     Q_ASSERT(_ircChannel);
893
894     int categoryId = UserCategoryItem::categoryFromModes(_ircChannel->userModes(ircUser));
895     UserCategoryItem* categoryItem = findCategoryItem(categoryId);
896
897     if (categoryItem) {
898         if (categoryItem->findIrcUser(ircUser)) {
899             return;  // already in the right category;
900         }
901     }
902     else {
903         categoryItem = new UserCategoryItem(categoryId, this);
904         newChild(categoryItem);
905     }
906
907     // find the item that needs reparenting
908     IrcUserItem* ircUserItem = nullptr;
909     for (int i = 0; i < childCount(); i++) {
910         auto* oldCategoryItem = qobject_cast<UserCategoryItem*>(child(i));
911         Q_ASSERT(oldCategoryItem);
912         IrcUserItem* userItem = oldCategoryItem->findIrcUser(ircUser);
913         if (userItem) {
914             ircUserItem = userItem;
915             break;
916         }
917     }
918
919     if (!ircUserItem) {
920         qWarning() << "ChannelBufferItem::userModeChanged(IrcUser *): unable to determine old category of" << ircUser;
921         return;
922     }
923     ircUserItem->reParent(categoryItem);
924 }
925
926 /*****************************************
927  *  User Category Items (like @vh etc.)
928  *****************************************/
929 // we hardcode this even though we have PREFIX in network... but that wouldn't help with mapping modes to
930 // category strings anyway.
931 const QList<QChar> UserCategoryItem::categories = QList<QChar>() << 'q' << 'a' << 'o' << 'h' << 'v';
932
933 UserCategoryItem::UserCategoryItem(int category, AbstractTreeItem* parent)
934     : PropertyMapItem(parent)
935     , _category(category)
936 {
937     setFlags(Qt::ItemIsEnabled);
938     setTreeItemFlags(AbstractTreeItem::DeleteOnLastChildRemoved);
939     setObjectName(parent->data(0, Qt::DisplayRole).toString() + "/" + QString::number(category));
940 }
941
942 QStringList UserCategoryItem::propertyOrder() const
943 {
944     static QStringList order{"categoryName"};
945     return order;
946 }
947
948 // caching this makes no sense, since we display the user number dynamically
949 QString UserCategoryItem::categoryName() const
950 {
951     int n = childCount();
952     switch (_category) {
953     case 0:
954         return tr("%n Owner(s)", "", n);
955     case 1:
956         return tr("%n Admin(s)", "", n);
957     case 2:
958         return tr("%n Operator(s)", "", n);
959     case 3:
960         return tr("%n Half-Op(s)", "", n);
961     case 4:
962         return tr("%n Voiced", "", n);
963     default:
964         return tr("%n User(s)", "", n);
965     }
966 }
967
968 IrcUserItem* UserCategoryItem::findIrcUser(IrcUser* ircUser)
969 {
970     IrcUserItem* userItem = nullptr;
971
972     for (int i = 0; i < childCount(); i++) {
973         userItem = qobject_cast<IrcUserItem*>(child(i));
974         if (!userItem)
975             continue;
976         if (userItem->ircUser() == ircUser)
977             return userItem;
978     }
979     return nullptr;
980 }
981
982 void UserCategoryItem::addUsers(const QList<IrcUser*>& ircUsers)
983 {
984     QList<AbstractTreeItem*> userItems;
985     foreach (IrcUser* ircUser, ircUsers)
986         userItems << new IrcUserItem(ircUser, this);
987     newChilds(userItems);
988     emit dataChanged(0);
989 }
990
991 bool UserCategoryItem::removeUser(IrcUser* ircUser)
992 {
993     IrcUserItem* userItem = findIrcUser(ircUser);
994     auto success = (bool)userItem;
995     if (success) {
996         removeChild(userItem);
997         emit dataChanged(0);
998     }
999     return success;
1000 }
1001
1002 int UserCategoryItem::categoryFromModes(const QString& modes)
1003 {
1004     for (int i = 0; i < categories.count(); i++) {
1005         if (modes.contains(categories[i]))
1006             return i;
1007     }
1008     return categories.count();
1009 }
1010
1011 QVariant UserCategoryItem::data(int column, int role) const
1012 {
1013     switch (role) {
1014     case TreeModel::SortRole:
1015         return _category;
1016     case NetworkModel::ItemActiveRole:
1017         return true;
1018     case NetworkModel::ItemTypeRole:
1019         return NetworkModel::UserCategoryItemType;
1020     case NetworkModel::BufferIdRole:
1021         return parent()->data(column, role);
1022     case NetworkModel::NetworkIdRole:
1023         return parent()->data(column, role);
1024     case NetworkModel::BufferInfoRole:
1025         return parent()->data(column, role);
1026     default:
1027         return PropertyMapItem::data(column, role);
1028     }
1029 }
1030
1031 /*****************************************
1032  *  Irc User Items
1033  *****************************************/
1034 IrcUserItem::IrcUserItem(IrcUser* ircUser, AbstractTreeItem* parent)
1035     : PropertyMapItem(parent)
1036     , _ircUser(ircUser)
1037 {
1038     setObjectName(ircUser->nick());
1039     connect(ircUser, &IrcUser::quited, this, &IrcUserItem::ircUserQuited);
1040     connect(ircUser, &IrcUser::nickSet, this, [this]() { emit dataChanged(); });
1041     connect(ircUser, &IrcUser::awaySet, this, [this]() { emit dataChanged(); });
1042 }
1043
1044 QStringList IrcUserItem::propertyOrder() const
1045 {
1046     static QStringList order{"nickName"};
1047     return order;
1048 }
1049
1050 QVariant IrcUserItem::data(int column, int role) const
1051 {
1052     switch (role) {
1053     case NetworkModel::ItemActiveRole:
1054         return isActive();
1055     case NetworkModel::ItemTypeRole:
1056         return NetworkModel::IrcUserItemType;
1057     case NetworkModel::BufferIdRole:
1058         return parent()->data(column, role);
1059     case NetworkModel::NetworkIdRole:
1060         return parent()->data(column, role);
1061     case NetworkModel::BufferInfoRole:
1062         return parent()->data(column, role);
1063     case NetworkModel::IrcChannelRole:
1064         return parent()->data(column, role);
1065     case NetworkModel::IrcUserRole:
1066         return QVariant::fromValue(_ircUser.data());
1067     case NetworkModel::UserAwayRole:
1068         return (bool)_ircUser ? _ircUser->isAway() : false;
1069     default:
1070         return PropertyMapItem::data(column, role);
1071     }
1072 }
1073
1074 QString IrcUserItem::toolTip(int column) const
1075 {
1076     Q_UNUSED(column);
1077     QString strTooltip;
1078     QTextStream tooltip(&strTooltip, QIODevice::WriteOnly);
1079     tooltip << "<qt><style>.bold { font-weight: bold; } .italic { font-style: italic; }</style>";
1080
1081     // Keep track of whether or not information has been added
1082     bool infoAdded = false;
1083
1084     // Use bufferName() for QueryBufferItem, nickName() for IrcUserItem
1085     tooltip << "<p class='bold' align='center'>" << NetworkItem::escapeHTML(nickName(), true);
1086     if (_ircUser->userModes() != "") {
1087         // TODO: Translate user Modes and add them to the table below and in QueryBufferItem::toolTip
1088         tooltip << " (" << _ircUser->userModes() << ")";
1089     }
1090     tooltip << "</p>";
1091
1092     auto addRow = [&](const QString& key, const QString& value, bool condition) {
1093         if (condition) {
1094             tooltip << "<tr><td class='bold' align='right'>" << key << "</td><td>" << value << "</td></tr>";
1095             infoAdded = true;
1096         }
1097     };
1098
1099     tooltip << "<table cellspacing='5' cellpadding='0'>";
1100     addRow(tr("Modes"), NetworkItem::escapeHTML(channelModes()), !channelModes().isEmpty());
1101     if (_ircUser->isAway()) {
1102         QString awayMessageHTML = QString("<p class='italic'>%1</p>").arg(tr("Unknown"));
1103
1104         // If away message is known, replace with the escaped message.
1105         if (!_ircUser->awayMessage().isEmpty()) {
1106             awayMessageHTML = NetworkItem::escapeHTML(_ircUser->awayMessage());
1107         }
1108         addRow(NetworkItem::escapeHTML(tr("Away message"), true), awayMessageHTML, true);
1109     }
1110     addRow(tr("Realname"), NetworkItem::escapeHTML(_ircUser->realName()), !_ircUser->realName().isEmpty());
1111
1112     // suserHost may return "<nick> is available for help", which should be translated.
1113     // See https://www.alien.net.au/irc/irc2numerics.html
1114     if (_ircUser->suserHost().endsWith("available for help")) {
1115         addRow(NetworkItem::escapeHTML(tr("Help status"), true), NetworkItem::escapeHTML(tr("Available for help")), true);
1116     }
1117     else {
1118         addRow(NetworkItem::escapeHTML(tr("Service status"), true),
1119                NetworkItem::escapeHTML(_ircUser->suserHost()),
1120                !_ircUser->suserHost().isEmpty());
1121     }
1122
1123     // Keep track of whether or not the account information's been added.  Don't show it twice.
1124     bool accountAdded = false;
1125     if (!_ircUser->account().isEmpty()) {
1126         // IRCv3 account-notify is supported by the core and IRC server.
1127         // Assume logged out (seems to be more common)
1128         QString accountHTML = QString("<p class='italic'>%1</p>").arg(tr("Not logged in"));
1129
1130         // If account is logged in, replace with the escaped account name.
1131         if (_ircUser->account() != "*") {
1132             accountHTML = NetworkItem::escapeHTML(_ircUser->account());
1133         }
1134         addRow(NetworkItem::escapeHTML(tr("Account"), true), accountHTML, true);
1135         // Mark the row as added
1136         accountAdded = true;
1137     }
1138     // whoisServiceReply may return "<nick> is identified for this nick", which should be translated.
1139     // See https://www.alien.net.au/irc/irc2numerics.html
1140     if (_ircUser->whoisServiceReply().endsWith("identified for this nick")) {
1141         addRow(NetworkItem::escapeHTML(tr("Account"), true), NetworkItem::escapeHTML(tr("Identified for this nick")), !accountAdded);
1142         // Don't add the account row again if information's already added via account-notify
1143         // Not used further down...
1144         // accountAdded = true;
1145     }
1146     else {
1147         addRow(NetworkItem::escapeHTML(tr("Service Reply"), true),
1148                NetworkItem::escapeHTML(_ircUser->whoisServiceReply()),
1149                !_ircUser->whoisServiceReply().isEmpty());
1150     }
1151     addRow(tr("Hostmask"),
1152            NetworkItem::escapeHTML(_ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!") + 1)),
1153            !(_ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!") + 1) == "@"));
1154     // ircOperator may contain "is an" or "is a", which should be removed.
1155     addRow(tr("Operator"),
1156            NetworkItem::escapeHTML(_ircUser->ircOperator().replace("is an ", "").replace("is a ", "")),
1157            !_ircUser->ircOperator().isEmpty());
1158
1159     if (_ircUser->idleTime().isValid()) {
1160         QDateTime now = QDateTime::currentDateTime();
1161         QDateTime idle = _ircUser->idleTime();
1162         int idleTime = idle.secsTo(now);
1163         addRow(NetworkItem::escapeHTML(tr("Idling since"), true), secondsToString(idleTime), true);
1164     }
1165
1166     if (_ircUser->loginTime().isValid()) {
1167         addRow(NetworkItem::escapeHTML(tr("Login time"), true), _ircUser->loginTime().toString(), true);
1168     }
1169
1170     addRow(tr("Server"), NetworkItem::escapeHTML(_ircUser->server()), !_ircUser->server().isEmpty());
1171     tooltip << "</table>";
1172
1173     // If no further information found, offer an explanatory message
1174     if (!infoAdded)
1175         tooltip << "<p class='italic' align='center'>" << tr("No information available") << "</p>";
1176
1177     tooltip << "</qt>";
1178     return strTooltip;
1179 }
1180
1181 QString IrcUserItem::channelModes() const
1182 {
1183     // IrcUserItems are parented to UserCategoryItem, which are parented to ChannelBufferItem.
1184     // We want the channel buffer item in order to get the channel-specific user modes.
1185     auto* category = qobject_cast<UserCategoryItem*>(parent());
1186     if (!category)
1187         return QString();
1188
1189     auto* channel = qobject_cast<ChannelBufferItem*>(category->parent());
1190     if (!channel)
1191         return QString();
1192
1193     return channel->nickChannelModes(nickName());
1194 }
1195
1196 /*****************************************
1197  * NetworkModel
1198  *****************************************/
1199 NetworkModel::NetworkModel(QObject* parent)
1200     : TreeModel(NetworkModel::defaultHeader(), parent)
1201 {
1202     connect(this, &NetworkModel::rowsInserted, this, &NetworkModel::checkForNewBuffers);
1203     connect(this, &NetworkModel::rowsAboutToBeRemoved, this, &NetworkModel::checkForRemovedBuffers);
1204
1205     BufferSettings defaultSettings;
1206     defaultSettings.notify("UserNoticesTarget", this, &NetworkModel::messageRedirectionSettingsChanged);
1207     defaultSettings.notify("ServerNoticesTarget", this, &NetworkModel::messageRedirectionSettingsChanged);
1208     defaultSettings.notify("ErrorMsgsTarget", this, &NetworkModel::messageRedirectionSettingsChanged);
1209     messageRedirectionSettingsChanged();
1210 }
1211
1212 QList<QVariant> NetworkModel::defaultHeader()
1213 {
1214     QList<QVariant> data;
1215     data << tr("Chat") << tr("Topic") << tr("Nick Count");
1216     return data;
1217 }
1218
1219 bool NetworkModel::isBufferIndex(const QModelIndex& index) const
1220 {
1221     return index.data(NetworkModel::ItemTypeRole) == NetworkModel::BufferItemType;
1222 }
1223
1224 int NetworkModel::networkRow(NetworkId networkId) const
1225 {
1226     NetworkItem* netItem = nullptr;
1227     for (int i = 0; i < rootItem->childCount(); i++) {
1228         netItem = qobject_cast<NetworkItem*>(rootItem->child(i));
1229         if (!netItem)
1230             continue;
1231         if (netItem->networkId() == networkId)
1232             return i;
1233     }
1234     return -1;
1235 }
1236
1237 QModelIndex NetworkModel::networkIndex(NetworkId networkId)
1238 {
1239     int netRow = networkRow(networkId);
1240     if (netRow == -1)
1241         return {};
1242     else
1243         return indexByItem(qobject_cast<NetworkItem*>(rootItem->child(netRow)));
1244 }
1245
1246 NetworkItem* NetworkModel::findNetworkItem(NetworkId networkId) const
1247 {
1248     int netRow = networkRow(networkId);
1249     if (netRow == -1)
1250         return nullptr;
1251     else
1252         return qobject_cast<NetworkItem*>(rootItem->child(netRow));
1253 }
1254
1255 NetworkItem* NetworkModel::networkItem(NetworkId networkId)
1256 {
1257     NetworkItem* netItem = findNetworkItem(networkId);
1258
1259     if (netItem == nullptr) {
1260         netItem = new NetworkItem(networkId, rootItem);
1261         rootItem->newChild(netItem);
1262     }
1263     return netItem;
1264 }
1265
1266 void NetworkModel::networkRemoved(const NetworkId& networkId)
1267 {
1268     int netRow = networkRow(networkId);
1269     if (netRow != -1) {
1270         rootItem->removeChild(netRow);
1271     }
1272 }
1273
1274 QModelIndex NetworkModel::bufferIndex(BufferId bufferId)
1275 {
1276     if (!_bufferItemCache.contains(bufferId))
1277         return {};
1278
1279     return indexByItem(_bufferItemCache[bufferId]);
1280 }
1281
1282 BufferItem* NetworkModel::findBufferItem(BufferId bufferId) const
1283 {
1284     if (_bufferItemCache.contains(bufferId))
1285         return _bufferItemCache[bufferId];
1286     else
1287         return nullptr;
1288 }
1289
1290 BufferItem* NetworkModel::bufferItem(const BufferInfo& bufferInfo)
1291 {
1292     if (_bufferItemCache.contains(bufferInfo.bufferId()))
1293         return _bufferItemCache[bufferInfo.bufferId()];
1294
1295     NetworkItem* netItem = networkItem(bufferInfo.networkId());
1296     return netItem->bufferItem(bufferInfo);
1297 }
1298
1299 QStringList NetworkModel::mimeTypes() const
1300 {
1301     // mimetypes we accept for drops
1302     QStringList types;
1303     // comma separated list of colon separated pairs of networkid and bufferid
1304     // example: 0:1,0:2,1:4
1305     types << "application/Quassel/BufferItemList";
1306     return types;
1307 }
1308
1309 bool NetworkModel::mimeContainsBufferList(const QMimeData* mimeData)
1310 {
1311     return mimeData->hasFormat("application/Quassel/BufferItemList");
1312 }
1313
1314 QList<QPair<NetworkId, BufferId>> NetworkModel::mimeDataToBufferList(const QMimeData* mimeData)
1315 {
1316     QList<QPair<NetworkId, BufferId>> bufferList;
1317
1318     if (!mimeContainsBufferList(mimeData))
1319         return bufferList;
1320
1321     QStringList rawBufferList = QString::fromLatin1(mimeData->data("application/Quassel/BufferItemList")).split(",");
1322     NetworkId networkId;
1323     BufferId bufferUid;
1324     foreach (QString rawBuffer, rawBufferList) {
1325         if (!rawBuffer.contains(":"))
1326             continue;
1327         networkId = rawBuffer.section(":", 0, 0).toInt();
1328         bufferUid = rawBuffer.section(":", 1, 1).toInt();
1329         bufferList.append(qMakePair(networkId, bufferUid));
1330     }
1331     return bufferList;
1332 }
1333
1334 QMimeData* NetworkModel::mimeData(const QModelIndexList& indexes) const
1335 {
1336     auto* mimeData = new QMimeData();
1337
1338     QStringList bufferlist;
1339     QString netid, uid, bufferid;
1340     foreach (QModelIndex index, indexes) {
1341         netid = QString::number(index.data(NetworkIdRole).value<NetworkId>().toInt());
1342         uid = QString::number(index.data(BufferIdRole).value<BufferId>().toInt());
1343         bufferid = QString("%1:%2").arg(netid).arg(uid);
1344         if (!bufferlist.contains(bufferid))
1345             bufferlist << bufferid;
1346     }
1347
1348     mimeData->setData("application/Quassel/BufferItemList", bufferlist.join(",").toLatin1());
1349
1350     return mimeData;
1351 }
1352
1353 void NetworkModel::attachNetwork(Network* net)
1354 {
1355     NetworkItem* netItem = networkItem(net->networkId());
1356     netItem->attachNetwork(net);
1357 }
1358
1359 void NetworkModel::bufferUpdated(BufferInfo bufferInfo)
1360 {
1361     BufferItem* bufItem = bufferItem(bufferInfo);
1362     QModelIndex itemindex = indexByItem(bufItem);
1363     emit dataChanged(itemindex, itemindex);
1364 }
1365
1366 void NetworkModel::removeBuffer(BufferId bufferId)
1367 {
1368     BufferItem* buffItem = findBufferItem(bufferId);
1369     if (!buffItem)
1370         return;
1371
1372     buffItem->parent()->removeChild(buffItem);
1373 }
1374
1375 MsgId NetworkModel::lastSeenMsgId(BufferId bufferId) const
1376 {
1377     if (!_bufferItemCache.contains(bufferId))
1378         return {};
1379
1380     return _bufferItemCache[bufferId]->lastSeenMsgId();
1381 }
1382
1383 MsgId NetworkModel::markerLineMsgId(BufferId bufferId) const
1384 {
1385     if (!_bufferItemCache.contains(bufferId))
1386         return {};
1387
1388     return _bufferItemCache[bufferId]->markerLineMsgId();
1389 }
1390
1391 // FIXME we always seem to use this (expensive) non-const version
1392 MsgId NetworkModel::lastSeenMsgId(const BufferId& bufferId)
1393 {
1394     BufferItem* bufferItem = findBufferItem(bufferId);
1395     if (!bufferItem) {
1396         qDebug() << "NetworkModel::lastSeenMsgId(): buffer is unknown:" << bufferId;
1397         Client::purgeKnownBufferIds();
1398         return {};
1399     }
1400     return bufferItem->lastSeenMsgId();
1401 }
1402
1403 void NetworkModel::setLastSeenMsgId(const BufferId& bufferId, const MsgId& msgId)
1404 {
1405     BufferItem* bufferItem = findBufferItem(bufferId);
1406     if (!bufferItem) {
1407         qDebug() << "NetworkModel::setLastSeenMsgId(): buffer is unknown:" << bufferId;
1408         Client::purgeKnownBufferIds();
1409         return;
1410     }
1411     bufferItem->setLastSeenMsgId(msgId);
1412     emit lastSeenMsgSet(bufferId, msgId);
1413 }
1414
1415 void NetworkModel::setMarkerLineMsgId(const BufferId& bufferId, const MsgId& msgId)
1416 {
1417     BufferItem* bufferItem = findBufferItem(bufferId);
1418     if (!bufferItem) {
1419         qDebug() << "NetworkModel::setMarkerLineMsgId(): buffer is unknown:" << bufferId;
1420         Client::purgeKnownBufferIds();
1421         return;
1422     }
1423     bufferItem->setMarkerLineMsgId(msgId);
1424     emit markerLineSet(bufferId, msgId);
1425 }
1426
1427 void NetworkModel::updateBufferActivity(Message& msg)
1428 {
1429     int redirectionTarget = 0;
1430     switch (msg.type()) {
1431     case Message::Notice:
1432         if (bufferType(msg.bufferId()) != BufferInfo::ChannelBuffer) {
1433             msg.setFlags(msg.flags() | Message::Redirected);
1434             if (msg.flags() & Message::ServerMsg) {
1435                 // server notice
1436                 redirectionTarget = _serverNoticesTarget;
1437             }
1438             else {
1439                 redirectionTarget = _userNoticesTarget;
1440             }
1441         }
1442         break;
1443     case Message::Error:
1444         msg.setFlags(msg.flags() | Message::Redirected);
1445         redirectionTarget = _errorMsgsTarget;
1446         break;
1447     // Update IrcUser's last activity
1448     case Message::Plain:
1449     case Message::Action:
1450         if (bufferType(msg.bufferId()) == BufferInfo::ChannelBuffer) {
1451             const Network* net = Client::network(msg.bufferInfo().networkId());
1452             IrcUser* user = net ? net->ircUser(nickFromMask(msg.sender())) : nullptr;
1453             if (user)
1454                 user->setLastChannelActivity(msg.bufferId(), msg.timestamp());
1455         }
1456         break;
1457     default:
1458         break;
1459     }
1460
1461     if (msg.flags() & Message::Redirected) {
1462         if (redirectionTarget & BufferSettings::DefaultBuffer)
1463             updateBufferActivity(bufferItem(msg.bufferInfo()), msg);
1464
1465         if (redirectionTarget & BufferSettings::StatusBuffer) {
1466             const NetworkItem* netItem = findNetworkItem(msg.bufferInfo().networkId());
1467             if (netItem) {
1468                 updateBufferActivity(netItem->statusBufferItem(), msg);
1469             }
1470         }
1471     }
1472     else {
1473         if ((BufferSettings(msg.bufferId()).messageFilter() & msg.type()) != msg.type())
1474             updateBufferActivity(bufferItem(msg.bufferInfo()), msg);
1475     }
1476 }
1477
1478 void NetworkModel::updateBufferActivity(BufferItem* bufferItem, const Message& msg)
1479 {
1480     if (!bufferItem)
1481         return;
1482
1483     bufferItem->updateActivityLevel(msg);
1484     if (bufferItem->isCurrentBuffer())
1485         emit requestSetLastSeenMsg(bufferItem->bufferId(), msg.msgId());
1486 }
1487
1488 void NetworkModel::setBufferActivity(const BufferId& bufferId, BufferInfo::ActivityLevel level)
1489 {
1490     BufferItem* bufferItem = findBufferItem(bufferId);
1491     if (!bufferItem) {
1492         qDebug() << "NetworkModel::setBufferActivity(): buffer is unknown:" << bufferId;
1493         return;
1494     }
1495     bufferItem->setActivityLevel(level);
1496 }
1497
1498 void NetworkModel::clearBufferActivity(const BufferId& bufferId)
1499 {
1500     BufferItem* bufferItem = findBufferItem(bufferId);
1501     if (!bufferItem) {
1502         qDebug() << "NetworkModel::clearBufferActivity(): buffer is unknown:" << bufferId;
1503         return;
1504     }
1505     bufferItem->clearActivityLevel();
1506 }
1507
1508 const Network* NetworkModel::networkByIndex(const QModelIndex& index) const
1509 {
1510     QVariant netVariant = index.data(NetworkIdRole);
1511     if (!netVariant.isValid())
1512         return nullptr;
1513
1514     NetworkId networkId = netVariant.value<NetworkId>();
1515     return Client::network(networkId);
1516 }
1517
1518 void NetworkModel::checkForRemovedBuffers(const QModelIndex& parent, int start, int end)
1519 {
1520     if (parent.data(ItemTypeRole) != NetworkItemType)
1521         return;
1522
1523     for (int row = start; row <= end; row++) {
1524         _bufferItemCache.remove(index(row, 0, parent).data(BufferIdRole).value<BufferId>());
1525     }
1526 }
1527
1528 void NetworkModel::checkForNewBuffers(const QModelIndex& parent, int start, int end)
1529 {
1530     if (parent.data(ItemTypeRole) != NetworkItemType)
1531         return;
1532
1533     for (int row = start; row <= end; row++) {
1534         QModelIndex child = parent.model()->index(row, 0, parent);
1535         _bufferItemCache[child.data(BufferIdRole).value<BufferId>()] = static_cast<BufferItem*>(child.internalPointer());
1536     }
1537 }
1538
1539 QString NetworkModel::bufferName(BufferId bufferId) const
1540 {
1541     if (!_bufferItemCache.contains(bufferId))
1542         return QString();
1543
1544     return _bufferItemCache[bufferId]->bufferName();
1545 }
1546
1547 BufferInfo::Type NetworkModel::bufferType(BufferId bufferId) const
1548 {
1549     if (!_bufferItemCache.contains(bufferId))
1550         return BufferInfo::InvalidBuffer;
1551
1552     return _bufferItemCache[bufferId]->bufferType();
1553 }
1554
1555 BufferInfo NetworkModel::bufferInfo(BufferId bufferId) const
1556 {
1557     if (!_bufferItemCache.contains(bufferId))
1558         return BufferInfo();
1559
1560     return _bufferItemCache[bufferId]->bufferInfo();
1561 }
1562
1563 NetworkId NetworkModel::networkId(BufferId bufferId) const
1564 {
1565     if (!_bufferItemCache.contains(bufferId))
1566         return {};
1567
1568     auto* netItem = qobject_cast<NetworkItem*>(_bufferItemCache[bufferId]->parent());
1569     if (netItem)
1570         return netItem->networkId();
1571     else
1572         return {};
1573 }
1574
1575 QString NetworkModel::networkName(BufferId bufferId) const
1576 {
1577     if (!_bufferItemCache.contains(bufferId))
1578         return QString();
1579
1580     auto* netItem = qobject_cast<NetworkItem*>(_bufferItemCache[bufferId]->parent());
1581     if (netItem)
1582         return netItem->networkName();
1583     else
1584         return QString();
1585 }
1586
1587 BufferId NetworkModel::bufferId(NetworkId networkId, const QString& bufferName, Qt::CaseSensitivity cs) const
1588 {
1589     const NetworkItem* netItem = findNetworkItem(networkId);
1590     if (!netItem)
1591         return {};
1592
1593     for (int i = 0; i < netItem->childCount(); i++) {
1594         auto* bufferItem = qobject_cast<BufferItem*>(netItem->child(i));
1595         if (bufferItem && !bufferItem->bufferName().compare(bufferName, cs))
1596             return bufferItem->bufferId();
1597     }
1598     return {};
1599 }
1600
1601 void NetworkModel::sortBufferIds(QList<BufferId>& bufferIds) const
1602 {
1603     QList<BufferItem*> bufferItems;
1604     foreach (BufferId bufferId, bufferIds) {
1605         if (_bufferItemCache.contains(bufferId))
1606             bufferItems << _bufferItemCache[bufferId];
1607     }
1608
1609     std::sort(bufferItems.begin(), bufferItems.end(), bufferItemLessThan);
1610
1611     bufferIds.clear();
1612     foreach (BufferItem* bufferItem, bufferItems) {
1613         bufferIds << bufferItem->bufferId();
1614     }
1615 }
1616
1617 QList<BufferId> NetworkModel::allBufferIdsSorted() const
1618 {
1619     QList<BufferId> bufferIds = allBufferIds();
1620     sortBufferIds(bufferIds);
1621     return bufferIds;
1622 }
1623
1624 bool NetworkModel::bufferItemLessThan(const BufferItem* left, const BufferItem* right)
1625 {
1626     int leftType = left->bufferType();
1627     int rightType = right->bufferType();
1628
1629     if (leftType != rightType)
1630         return leftType < rightType;
1631     else
1632         return QString::compare(left->bufferName(), right->bufferName(), Qt::CaseInsensitive) < 0;
1633 }
1634
1635 void NetworkModel::messageRedirectionSettingsChanged()
1636 {
1637     BufferSettings bufferSettings;
1638
1639     _userNoticesTarget = bufferSettings.userNoticesTarget();
1640     _serverNoticesTarget = bufferSettings.serverNoticesTarget();
1641     _errorMsgsTarget = bufferSettings.errorMsgsTarget();
1642 }
1643
1644 void NetworkModel::bufferActivityChanged(BufferId bufferId, const Message::Types activity)
1645 {
1646     auto _bufferItem = findBufferItem(bufferId);
1647     if (!_bufferItem) {
1648         qDebug() << "NetworkModel::bufferActivityChanged(): buffer is unknown:" << bufferId;
1649         return;
1650     }
1651     auto hiddenTypes = BufferSettings(bufferId).messageFilter();
1652     auto visibleTypes = ~hiddenTypes;
1653     auto activityVisibleTypesIntersection = activity & visibleTypes;
1654     _bufferItem->setActivity(activityVisibleTypesIntersection, false);
1655 }
1656
1657 void NetworkModel::highlightCountChanged(BufferId bufferId, int count)
1658 {
1659     auto _bufferItem = findBufferItem(bufferId);
1660     if (!_bufferItem) {
1661         qDebug() << "NetworkModel::highlightCountChanged(): buffer is unknown:" << bufferId;
1662         return;
1663     }
1664     _bufferItem->addActivity(Message::Types{}, count > 0);
1665 }