modernize: Pass arguments by value and move in constructors
[quassel.git] / src / client / networkmodel.cpp
index 2adbe08..7b7f5bc 100644 (file)
@@ -1,5 +1,5 @@
 /***************************************************************************
- *   Copyright (C) 2005-2015 by the Quassel Project                        *
+ *   Copyright (C) 2005-2018 by the Quassel Project                        *
  *   devel@quassel-irc.org                                                 *
  *                                                                         *
  *   This program is free software; you can redistribute it and/or modify  *
@@ -22,9 +22,7 @@
 
 #include <QAbstractItemView>
 #include <QMimeData>
-#if QT_VERSION < 0x050000
-#include <QTextDocument>        // for Qt::escape()
-#endif
+#include <utility>
 
 #include "buffermodel.h"
 #include "buffersettings.h"
 #include "ircchannel.h"
 #include "network.h"
 #include "signalproxy.h"
+#include "buffersyncer.h"
 
 /*****************************************
 *  Network Items
 *****************************************/
 NetworkItem::NetworkItem(const NetworkId &netid, AbstractTreeItem *parent)
-    : PropertyMapItem(QList<QString>() << "networkName" << "currentServer" << "nickCount", parent),
+    : PropertyMapItem(parent),
     _networkId(netid),
-    _statusBufferItem(0)
+    _statusBufferItem(nullptr)
 {
     // DO NOT EMIT dataChanged() DIRECTLY IN NetworkItem
     // use networkDataChanged() instead. Otherwise you will end up in a infinite loop
@@ -52,6 +51,13 @@ NetworkItem::NetworkItem(const NetworkId &netid, AbstractTreeItem *parent)
 }
 
 
+QStringList NetworkItem::propertyOrder() const
+{
+    static QStringList order{"networkName", "currentServer", "nickCount"};
+    return order;
+}
+
+
 QVariant NetworkItem::data(int column, int role) const
 {
     switch (role) {
@@ -74,11 +80,19 @@ QVariant NetworkItem::data(int column, int role) const
     }
 }
 
+QString NetworkItem::escapeHTML(const QString &string, bool useNonbreakingSpaces)
+{
+    // QString.replace() doesn't guarantee the source string will remain constant.
+    // Use a local variable to avoid compiler errors.
+    QString formattedString = string.toHtmlEscaped();
+    return (useNonbreakingSpaces ? formattedString.replace(" ", "&nbsp;") : formattedString);
+}
+
 
 // FIXME shouldn't we check the bufferItemCache here?
 BufferItem *NetworkItem::findBufferItem(BufferId bufferId)
 {
-    BufferItem *bufferItem = 0;
+    BufferItem *bufferItem = nullptr;
 
     for (int i = 0; i < childCount(); i++) {
         bufferItem = qobject_cast<BufferItem *>(child(i));
@@ -87,7 +101,7 @@ BufferItem *NetworkItem::findBufferItem(BufferId bufferId)
         if (bufferItem->bufferId() == bufferId)
             return bufferItem;
     }
-    return 0;
+    return nullptr;
 }
 
 
@@ -133,6 +147,14 @@ BufferItem *NetworkItem::bufferItem(const BufferInfo &bufferInfo)
         break;
     }
 
+    BufferSyncer *bufferSyncer = Client::bufferSyncer();
+    if (bufferSyncer) {
+        bufferItem->addActivity(
+                bufferSyncer->activity(bufferItem->bufferId()),
+                bufferSyncer->highlightCount(bufferItem->bufferId()) > 0
+        );
+    }
+
     return bufferItem;
 }
 
@@ -179,7 +201,7 @@ void NetworkItem::attachIrcChannel(IrcChannel *ircChannel)
 
 void NetworkItem::attachIrcUser(IrcUser *ircUser)
 {
-    QueryBufferItem *queryItem = 0;
+    QueryBufferItem *queryItem = nullptr;
     for (int i = 0; i < childCount(); i++) {
         queryItem = qobject_cast<QueryBufferItem *>(child(i));
         if (!queryItem)
@@ -210,21 +232,31 @@ void NetworkItem::setCurrentServer(const QString &serverName)
 QString NetworkItem::toolTip(int column) const
 {
     Q_UNUSED(column);
+    QString strTooltip;
+    QTextStream tooltip( &strTooltip, QIODevice::WriteOnly );
+    tooltip << "<qt><style>.bold { font-weight: bold; } .italic { font-style: italic; }</style>";
+
+    // Function to add a row to the tooltip table
+    auto addRow = [&](const QString& key, const QString& value, bool condition) {
+        if (condition) {
+            tooltip << "<tr><td class='bold' align='right'>" << key << "</td><td>" << value << "</td></tr>";
+        }
+    };
 
-#if QT_VERSION < 0x050000
-    QStringList toolTip(QString("<b>%1</b>").arg(Qt::escape(networkName())));
-    toolTip.append(tr("Server: %1").arg(Qt::escape(currentServer())));
-#else
-    QStringList toolTip(QString("<b>%1</b>").arg(networkName().toHtmlEscaped()));
-    toolTip.append(tr("Server: %1").arg(currentServer().toHtmlEscaped()));
-#endif
-    toolTip.append(tr("Users: %1").arg(nickCount()));
+    tooltip << "<p class='bold' align='center'>" << NetworkItem::escapeHTML(networkName(), true) << "</p>";
+    if (isActive()) {
+        tooltip << "<table cellspacing='5' cellpadding='0'>";
+        addRow(tr("Server"), NetworkItem::escapeHTML(currentServer(), true), !currentServer().isEmpty());
+        addRow(tr("Users"), QString::number(nickCount()), true);
+        if (_network)
+            addRow(tr("Lag"), NetworkItem::escapeHTML(tr("%1 msecs").arg(_network->latency()), true), true);
 
-    if (_network) {
-        toolTip.append(tr("Lag: %1 msecs").arg(_network->latency()));
+        tooltip << "</table>";
+    } else {
+        tooltip << "<p class='italic' align='center'>" << tr("Not connected") << "</p>";
     }
-
-    return QString("<p> %1 </p>").arg(toolTip.join("<br />"));
+    tooltip << "</qt>";
+    return strTooltip;
 }
 
 
@@ -233,7 +265,7 @@ void NetworkItem::onBeginRemoveChilds(int start, int end)
     for (int i = start; i <= end; i++) {
         StatusBufferItem *statusBufferItem = qobject_cast<StatusBufferItem *>(child(i));
         if (statusBufferItem) {
-            _statusBufferItem = 0;
+            _statusBufferItem = nullptr;
             break;
         }
     }
@@ -242,7 +274,7 @@ void NetworkItem::onBeginRemoveChilds(int start, int end)
 
 void NetworkItem::onNetworkDestroyed()
 {
-    _network = 0;
+    _network = nullptr;
     emit networkDataChanged();
     removeAllChilds();
 }
@@ -251,15 +283,22 @@ void NetworkItem::onNetworkDestroyed()
 /*****************************************
 *  Fancy Buffer Items
 *****************************************/
-BufferItem::BufferItem(const BufferInfo &bufferInfo, AbstractTreeItem *parent)
-    : PropertyMapItem(QStringList() << "bufferName" << "topic" << "nickCount", parent),
-    _bufferInfo(bufferInfo),
+BufferItem::BufferItem(BufferInfo bufferInfo, AbstractTreeItem *parent)
+    : PropertyMapItem(parent),
+    _bufferInfo(std::move(bufferInfo)),
     _activity(BufferInfo::NoActivity)
 {
     setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
 }
 
 
+QStringList BufferItem::propertyOrder() const
+{
+    static QStringList order{"bufferName", "topic", "nickCount"};
+    return order;
+}
+
+
 void BufferItem::setActivityLevel(BufferInfo::ActivityLevel level)
 {
     if (_activity != level) {
@@ -271,11 +310,16 @@ void BufferItem::setActivityLevel(BufferInfo::ActivityLevel level)
 
 void BufferItem::clearActivityLevel()
 {
-    _activity = BufferInfo::NoActivity;
+    if (Client::isCoreFeatureEnabled(Quassel::Feature::BufferActivitySync)) {
+        // If the core handles activity sync, clear only the highlight flag
+        _activity &= ~BufferInfo::Highlight;
+    } else {
+        _activity = BufferInfo::NoActivity;
+    }
     _firstUnreadMsgId = MsgId();
 
     // FIXME remove with core proto v11
-    if (!(Client::coreFeatures() & Quassel::SynchronizedMarkerLine)) {
+    if (!Client::isCoreFeatureEnabled(Quassel::Feature::SynchronizedMarkerLine)) {
         _markerLineMsgId = _lastSeenMsgId;
     }
 
@@ -285,6 +329,11 @@ void BufferItem::clearActivityLevel()
 
 void BufferItem::updateActivityLevel(const Message &msg)
 {
+    // If the core handles activity, and this message is not a highlight, ignore this
+    if (Client::isCoreFeatureEnabled(Quassel::Feature::BufferActivitySync) && !msg.flags().testFlag(Message::Highlight)) {
+        return;
+    }
+
     if (isCurrentBuffer()) {
         return;
     }
@@ -305,19 +354,43 @@ void BufferItem::updateActivityLevel(const Message &msg)
         _firstUnreadMsgId = msg.msgId();
     }
 
+    Message::Types type;
+    // If the core handles activities, ignore types
+    if (Client::isCoreFeatureEnabled(Quassel::Feature::BufferActivitySync)) {
+        type = Message::Types();
+    } else {
+        type = msg.type();
+    }
+
+    if (addActivity(type, msg.flags().testFlag(Message::Highlight)) || stateChanged) {
+        emit dataChanged();
+    }
+}
+
+void BufferItem::setActivity(Message::Types type, bool highlight) {
     BufferInfo::ActivityLevel oldLevel = activityLevel();
 
-    _activity |= BufferInfo::OtherActivity;
-    if (msg.type() & (Message::Plain | Message::Notice | Message::Action))
+    _activity &= BufferInfo::Highlight;
+    addActivity(type, highlight);
+
+    if (_activity != oldLevel) {
+        emit dataChanged();
+    }
+}
+
+bool BufferItem::addActivity(Message::Types type, bool highlight) {
+    auto oldActivity = activityLevel();
+
+    if (type != Message::Types())
+        _activity |= BufferInfo::OtherActivity;
+
+    if (type.testFlag(Message::Plain) || type.testFlag(Message::Notice) || type.testFlag(Message::Action))
         _activity |= BufferInfo::NewMessage;
 
-    if (msg.flags() & Message::Highlight)
+    if (highlight)
         _activity |= BufferInfo::Highlight;
 
-    stateChanged |= (oldLevel != _activity);
-
-    if (stateChanged)
-        emit dataChanged();
+    return oldActivity != _activity;
 }
 
 
@@ -373,7 +446,7 @@ void BufferItem::setLastSeenMsgId(MsgId msgId)
     _lastSeenMsgId = msgId;
 
     // FIXME remove with core protocol v11
-    if (!(Client::coreFeatures() & Quassel::SynchronizedMarkerLine)) {
+    if (!Client::isCoreFeatureEnabled(Quassel::Feature::SynchronizedMarkerLine)) {
         if (!isCurrentBuffer())
             _markerLineMsgId = msgId;
     }
@@ -426,7 +499,7 @@ QString StatusBufferItem::toolTip(int column) const
 *****************************************/
 QueryBufferItem::QueryBufferItem(const BufferInfo &bufferInfo, NetworkItem *parent)
     : BufferItem(bufferInfo, parent),
-    _ircUser(0)
+    _ircUser(nullptr)
 {
     setFlags(flags() | Qt::ItemIsDropEnabled | Qt::ItemIsEditable);
 
@@ -463,6 +536,12 @@ bool QueryBufferItem::setData(int column, const QVariant &value, int role)
     case Qt::EditRole:
     {
         QString newName = value.toString();
+
+        // Sanity check - buffer names must not contain newlines!
+        int nlpos = newName.indexOf('\n');
+        if (nlpos >= 0)
+            newName = newName.left(nlpos);
+
         if (!newName.isEmpty()) {
             Client::renameBuffer(bufferId(), newName);
             return true;
@@ -492,36 +571,120 @@ QString QueryBufferItem::toolTip(int column) const
 {
     // pretty much code duplication of IrcUserItem::toolTip() but inheritance won't solve this...
     Q_UNUSED(column);
-    QStringList toolTip;
+    QString strTooltip;
+    QTextStream tooltip( &strTooltip, QIODevice::WriteOnly );
+    tooltip << "<qt><style>.bold { font-weight: bold; } .italic { font-style: italic; }</style>";
+
+    // Keep track of whether or not information has been added
+    bool infoAdded = false;
+
+    // Use bufferName() for QueryBufferItem, nickName() for IrcUserItem
+    tooltip << "<p class='bold' align='center'>";
+    tooltip << tr("Query with %1").arg(NetworkItem::escapeHTML(bufferName(), true));
+    if (!_ircUser) {
+        // User seems to be offline, let the no information message be added below
+        tooltip << "</p>";
+    } else {
+        // Function to add a row to the tooltip table
+        auto addRow = [&](const QString& key, const QString& value, bool condition) {
+            if (condition) {
+                tooltip << "<tr><td class='bold' align='right'>" << key << "</td><td>" << value << "</td></tr>";
+                infoAdded = true;
+            }
+        };
 
-    toolTip.append(tr("<b>Query with %1</b>").arg(bufferName()));
+        // User information is available
+        if (_ircUser->userModes() != "") {
+            //TODO Translate user Modes and add them to the table below and in IrcUserItem::toolTip
+            tooltip << " (" << _ircUser->userModes() << ")";
+        }
+        tooltip << "</p>";
 
-    if (_ircUser) {
-        if (_ircUser->userModes() != "") toolTip[0].append(QString(" (+%1)").arg(_ircUser->userModes()));
+        tooltip << "<table cellspacing='5' cellpadding='0'>";
         if (_ircUser->isAway()) {
-            toolTip[0].append(QString(" (away%1)").arg(!_ircUser->awayMessage().isEmpty() ? (QString(" ") + _ircUser->awayMessage()) : QString()));
+            QString awayMessageHTML = QString("<p class='italic'>%1</p>").arg(tr("Unknown"));
+
+            // If away message is known, replace with the escaped message.
+            if (!_ircUser->awayMessage().isEmpty()) {
+                awayMessageHTML = NetworkItem::escapeHTML(_ircUser->awayMessage());
+            }
+            addRow(NetworkItem::escapeHTML(tr("Away message"), true), awayMessageHTML, true);
+        }
+        addRow(tr("Realname"),
+               NetworkItem::escapeHTML(_ircUser->realName()),
+               !_ircUser->realName().isEmpty());
+        // suserHost may return "<nick> is available for help", which should be translated.
+        // See https://www.alien.net.au/irc/irc2numerics.html
+        if(_ircUser->suserHost().endsWith("available for help")) {
+            addRow(NetworkItem::escapeHTML(tr("Help status"), true),
+                   NetworkItem::escapeHTML(tr("Available for help")),
+                   true);
+        } else {
+            addRow(NetworkItem::escapeHTML(tr("Service status"), true),
+                   NetworkItem::escapeHTML(_ircUser->suserHost()),
+                   !_ircUser->suserHost().isEmpty());
         }
-        if (!_ircUser->realName().isEmpty()) toolTip.append(_ircUser->realName());
-        if (!_ircUser->ircOperator().isEmpty()) toolTip.append(QString("%1 %2").arg(_ircUser->nick()).arg(_ircUser->ircOperator()));
-        if (!_ircUser->suserHost().isEmpty()) toolTip.append(_ircUser->suserHost());
-        if (!_ircUser->whoisServiceReply().isEmpty()) toolTip.append(_ircUser->whoisServiceReply());
 
-        toolTip.append(_ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!")+1));
+        // Keep track of whether or not the account information's been added.  Don't show it twice.
+        bool accountAdded = false;
+        if(!_ircUser->account().isEmpty()) {
+            // IRCv3 account-notify is supported by the core and IRC server.
+            // Assume logged out (seems to be more common)
+            QString accountHTML = QString("<p class='italic'>%1</p>").arg(tr("Not logged in"));
+
+            // If account is logged in, replace with the escaped account name.
+            if (_ircUser->account() != "*") {
+                accountHTML = NetworkItem::escapeHTML(_ircUser->account());
+            }
+            addRow(NetworkItem::escapeHTML(tr("Account"), true),
+                   accountHTML,
+                   true);
+            // Mark the row as added
+            accountAdded = true;
+        }
+        // whoisServiceReply may return "<nick> is identified for this nick", which should be translated.
+        // See https://www.alien.net.au/irc/irc2numerics.html
+        if(_ircUser->whoisServiceReply().endsWith("identified for this nick")) {
+            addRow(NetworkItem::escapeHTML(tr("Account"), true),
+                   NetworkItem::escapeHTML(tr("Identified for this nick")),
+                   !accountAdded);
+            // Don't add the account row again if information's already added via account-notify
+            // Not used further down...
+            // accountAdded = true;
+        } else {
+            addRow(NetworkItem::escapeHTML(tr("Service Reply"), true),
+                   NetworkItem::escapeHTML(_ircUser->whoisServiceReply()),
+                   !_ircUser->whoisServiceReply().isEmpty());
+        }
+        addRow(tr("Hostmask"),
+               NetworkItem::escapeHTML(_ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!") + 1)),
+               !(_ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!") + 1) == "@"));
+        // ircOperator may contain "is an" or "is a", which should be removed.
+        addRow(tr("Operator"),
+               NetworkItem::escapeHTML(_ircUser->ircOperator().replace("is an ", "").replace("is a ", "")),
+               !_ircUser->ircOperator().isEmpty());
 
         if (_ircUser->idleTime().isValid()) {
             QDateTime now = QDateTime::currentDateTime();
             QDateTime idle = _ircUser->idleTime();
             int idleTime = idle.secsTo(now);
-            toolTip.append(tr("idling since %1").arg(secondsToString(idleTime)));
+            addRow(NetworkItem::escapeHTML(tr("Idling since"), true), secondsToString(idleTime), true);
         }
+
         if (_ircUser->loginTime().isValid()) {
-            toolTip.append(tr("login time: %1").arg(_ircUser->loginTime().toString()));
+            addRow(NetworkItem::escapeHTML(tr("Login time"), true), _ircUser->loginTime().toString(), true);
         }
 
-        if (!_ircUser->server().isEmpty()) toolTip.append(tr("server: %1").arg(_ircUser->server()));
+        addRow(tr("Server"), NetworkItem::escapeHTML(_ircUser->server()), !_ircUser->server().isEmpty());
+        tooltip << "</table>";
     }
 
-    return QString("<p> %1 </p>").arg(toolTip.join("<br />"));
+    // If no further information found, offer an explanatory message
+    if (!infoAdded)
+        tooltip << "<p class='italic' align='center'>" << tr("No information available") << "</p>";
+
+    tooltip << "</qt>";
+    return strTooltip;
 }
 
 
@@ -531,7 +694,7 @@ void QueryBufferItem::setIrcUser(IrcUser *ircUser)
         return;
 
     if (_ircUser) {
-        disconnect(_ircUser, 0, this, 0);
+        disconnect(_ircUser, nullptr, this, nullptr);
     }
 
     if (ircUser) {
@@ -548,8 +711,22 @@ void QueryBufferItem::setIrcUser(IrcUser *ircUser)
 
 void QueryBufferItem::removeIrcUser()
 {
-    _ircUser = 0;
-    emit dataChanged();
+    if (_ircUser) {
+        // Disconnect the active IrcUser before removing it, otherwise it will fire removeIrcUser()
+        // a second time when the object's destroyed due to QueryBufferItem::setIrcUser() connecting
+        // SIGNAL destroyed(QObject*) to SLOT removeIrcUser().
+        // This fixes removing an active IrcUser if the user had quit then rejoined in a nonstandard
+        // manner (e.g. updateNickFromHost calling newIrcUser, triggered by an away-notify message).
+        disconnect(_ircUser, nullptr, this, nullptr);
+
+        // Clear IrcUser (only set to 0 if not already 0)
+        _ircUser = nullptr;
+
+        // Only emit dataChanged() if data actually changed.  This might serve as a small
+        // optimization, but it can be moved outside the if statement if other behavior depends on
+        // it always being called.
+        emit dataChanged();
+    }
 }
 
 
@@ -558,8 +735,9 @@ void QueryBufferItem::removeIrcUser()
 *****************************************/
 ChannelBufferItem::ChannelBufferItem(const BufferInfo &bufferInfo, AbstractTreeItem *parent)
     : BufferItem(bufferInfo, parent),
-    _ircChannel(0)
+    _ircChannel(nullptr)
 {
+    setFlags(flags() | Qt::ItemIsDropEnabled);
 }
 
 
@@ -577,20 +755,28 @@ QVariant ChannelBufferItem::data(int column, int role) const
 QString ChannelBufferItem::toolTip(int column) const
 {
     Q_UNUSED(column);
-    QStringList toolTip;
+    QString strTooltip;
+    QTextStream tooltip( &strTooltip, QIODevice::WriteOnly );
+    tooltip << "<qt><style>.bold { font-weight: bold; } .italic { font-style: italic; }</style>";
+
+    // Function to add a row to the tooltip table
+    auto addRow = [&](const QString& key, const QString& value, bool condition) {
+        if (condition) {
+            tooltip << "<tr><td class='bold' align='right'>" << key << "</td><td>" << value << "</td></tr>";
+        }
+    };
+
+    tooltip << "<p class='bold' align='center'>";
+    tooltip << NetworkItem::escapeHTML(tr("Channel %1").arg(bufferName()), true) << "</p>";
 
-#if QT_VERSION < 0x050000
-    toolTip.append(tr("<b>Channel %1</b>").arg(Qt::escape(bufferName())));
-#else
-    toolTip.append(tr("<b>Channel %1</b>").arg(bufferName().toHtmlEscaped()));
-#endif
     if (isActive()) {
-        //TODO: add channel modes
-        toolTip.append(tr("<b>Users:</b> %1").arg(nickCount()));
+        tooltip << "<table cellspacing='5' cellpadding='0'>";
+        addRow(tr("Users"), QString::number(nickCount()), true);
+
         if (_ircChannel) {
             QString channelMode = _ircChannel->channelModeString(); // channelModeString is compiled on the fly -> thus cache the result
             if (!channelMode.isEmpty())
-                toolTip.append(tr("<b>Mode:</b> %1").arg(channelMode));
+                addRow(tr("Mode"), channelMode, true);
         }
 
         ItemViewSettings s;
@@ -599,21 +785,18 @@ QString ChannelBufferItem::toolTip(int column) const
             QString _topic = topic();
             if (_topic != "") {
                 _topic = stripFormatCodes(_topic);
-#if QT_VERSION < 0x050000
-                _topic = Qt::escape(_topic);
-#else
-                _topic = _topic.toHtmlEscaped();
-#endif
-                toolTip.append(QString("<font size='-2'>&nbsp;</font>"));
-                toolTip.append(tr("<b>Topic:</b> %1").arg(_topic));
+                _topic = NetworkItem::escapeHTML(_topic);
+                addRow(tr("Topic"), _topic, true);
             }
         }
-    }
-    else {
-        toolTip.append(tr("Not active <br /> Double-click to join"));
+
+        tooltip << "</table>";
+    } else {
+        tooltip << "<p class='italic' align='center'>" << tr("Not active, double-click to join") << "</p>";
     }
 
-    return tr("<p> %1 </p>").arg(toolTip.join("<br />"));
+    tooltip << "</qt>";
+    return strTooltip;
 }
 
 
@@ -621,7 +804,7 @@ void ChannelBufferItem::attachIrcChannel(IrcChannel *ircChannel)
 {
     if (_ircChannel) {
         qWarning() << Q_FUNC_INFO << "IrcChannel already set; cleanup failed!?";
-        disconnect(_ircChannel, 0, this, 0);
+        disconnect(_ircChannel, nullptr, this, nullptr);
     }
 
     _ircChannel = ircChannel;
@@ -651,12 +834,22 @@ void ChannelBufferItem::attachIrcChannel(IrcChannel *ircChannel)
     emit dataChanged();
 }
 
+QString ChannelBufferItem::nickChannelModes(const QString &nick) const
+{
+    if (!_ircChannel) {
+        qDebug() << Q_FUNC_INFO << "IrcChannel not set, can't get user modes";
+        return QString();
+    }
+
+    return _ircChannel->userModes(nick);
+}
+
 
 void ChannelBufferItem::ircChannelParted()
 {
     Q_CHECK_PTR(_ircChannel);
-    disconnect(_ircChannel, 0, this, 0);
-    _ircChannel = 0;
+    disconnect(_ircChannel, nullptr, this, nullptr);
+    _ircChannel = nullptr;
     emit dataChanged();
     removeAllChilds();
 }
@@ -665,7 +858,7 @@ void ChannelBufferItem::ircChannelParted()
 void ChannelBufferItem::ircChannelDestroyed()
 {
     if (_ircChannel) {
-        _ircChannel = 0;
+        _ircChannel = nullptr;
         emit dataChanged();
         removeAllChilds();
     }
@@ -681,7 +874,7 @@ void ChannelBufferItem::join(const QList<IrcUser *> &ircUsers)
 
 UserCategoryItem *ChannelBufferItem::findCategoryItem(int categoryId)
 {
-    UserCategoryItem *categoryItem = 0;
+    UserCategoryItem *categoryItem = nullptr;
 
     for (int i = 0; i < childCount(); i++) {
         categoryItem = qobject_cast<UserCategoryItem *>(child(i));
@@ -690,7 +883,7 @@ UserCategoryItem *ChannelBufferItem::findCategoryItem(int categoryId)
         if (categoryItem->categoryId() == categoryId)
             return categoryItem;
     }
-    return 0;
+    return nullptr;
 }
 
 
@@ -707,7 +900,7 @@ void ChannelBufferItem::addUsersToCategory(const QList<IrcUser *> &ircUsers)
     QHash<UserCategoryItem *, QList<IrcUser *> > categories;
 
     int categoryId = -1;
-    UserCategoryItem *categoryItem = 0;
+    UserCategoryItem *categoryItem = nullptr;
 
     foreach(IrcUser *ircUser, ircUsers) {
         categoryId = UserCategoryItem::categoryFromModes(_ircChannel->userModes(ircUser));
@@ -735,7 +928,7 @@ void ChannelBufferItem::part(IrcUser *ircUser)
         return;
     }
 
-    disconnect(ircUser, 0, this, 0);
+    disconnect(ircUser, nullptr, this, nullptr);
     removeUserFromCategory(ircUser);
     emit dataChanged(2);
 }
@@ -750,7 +943,7 @@ void ChannelBufferItem::removeUserFromCategory(IrcUser *ircUser)
         return;
     }
 
-    UserCategoryItem *categoryItem = 0;
+    UserCategoryItem *categoryItem = nullptr;
     for (int i = 0; i < childCount(); i++) {
         categoryItem = qobject_cast<UserCategoryItem *>(child(i));
         if (categoryItem->removeUser(ircUser)) {
@@ -780,7 +973,7 @@ void ChannelBufferItem::userModeChanged(IrcUser *ircUser)
     }
 
     // find the item that needs reparenting
-    IrcUserItem *ircUserItem = 0;
+    IrcUserItem *ircUserItem = nullptr;
     for (int i = 0; i < childCount(); i++) {
         UserCategoryItem *oldCategoryItem = qobject_cast<UserCategoryItem *>(child(i));
         Q_ASSERT(oldCategoryItem);
@@ -807,7 +1000,7 @@ void ChannelBufferItem::userModeChanged(IrcUser *ircUser)
 const QList<QChar> UserCategoryItem::categories = QList<QChar>() << 'q' << 'a' << 'o' << 'h' << 'v';
 
 UserCategoryItem::UserCategoryItem(int category, AbstractTreeItem *parent)
-    : PropertyMapItem(QStringList() << "categoryName", parent),
+    : PropertyMapItem(parent),
     _category(category)
 {
     setFlags(Qt::ItemIsEnabled);
@@ -816,30 +1009,37 @@ UserCategoryItem::UserCategoryItem(int category, AbstractTreeItem *parent)
 }
 
 
+QStringList UserCategoryItem::propertyOrder() const
+{
+    static QStringList order{"categoryName"};
+    return order;
+}
+
+
 // caching this makes no sense, since we display the user number dynamically
 QString UserCategoryItem::categoryName() const
 {
     int n = childCount();
     switch (_category) {
     case 0:
-        return tr("%n Owner(s)", 0, n);
+        return tr("%n Owner(s)", "", n);
     case 1:
-        return tr("%n Admin(s)", 0, n);
+        return tr("%n Admin(s)", "", n);
     case 2:
-        return tr("%n Operator(s)", 0, n);
+        return tr("%n Operator(s)", "", n);
     case 3:
-        return tr("%n Half-Op(s)", 0, n);
+        return tr("%n Half-Op(s)", "", n);
     case 4:
-        return tr("%n Voiced", 0, n);
+        return tr("%n Voiced", "", n);
     default:
-        return tr("%n User(s)", 0, n);
+        return tr("%n User(s)", "", n);
     }
 }
 
 
 IrcUserItem *UserCategoryItem::findIrcUser(IrcUser *ircUser)
 {
-    IrcUserItem *userItem = 0;
+    IrcUserItem *userItem = nullptr;
 
     for (int i = 0; i < childCount(); i++) {
         userItem = qobject_cast<IrcUserItem *>(child(i));
@@ -848,7 +1048,7 @@ IrcUserItem *UserCategoryItem::findIrcUser(IrcUser *ircUser)
         if (userItem->ircUser() == ircUser)
             return userItem;
     }
-    return 0;
+    return nullptr;
 }
 
 
@@ -909,7 +1109,7 @@ QVariant UserCategoryItem::data(int column, int role) const
 *  Irc User Items
 *****************************************/
 IrcUserItem::IrcUserItem(IrcUser *ircUser, AbstractTreeItem *parent)
-    : PropertyMapItem(QStringList() << "nickName", parent),
+    : PropertyMapItem(parent),
     _ircUser(ircUser)
 {
     setObjectName(ircUser->nick());
@@ -919,6 +1119,13 @@ IrcUserItem::IrcUserItem(IrcUser *ircUser, AbstractTreeItem *parent)
 }
 
 
+QStringList IrcUserItem::propertyOrder() const
+{
+    static QStringList order{"nickName"};
+    return order;
+}
+
+
 QVariant IrcUserItem::data(int column, int role) const
 {
     switch (role) {
@@ -949,54 +1156,132 @@ QString IrcUserItem::toolTip(int column) const
     Q_UNUSED(column);
     QString strTooltip;
     QTextStream tooltip( &strTooltip, QIODevice::WriteOnly );
-    tooltip << "<qt><style>.bold { font-weight: bold; }</style>";
+    tooltip << "<qt><style>.bold { font-weight: bold; } .italic { font-style: italic; }</style>";
 
-    tooltip << "<p class='bold' align='center'>" << nickName();
+    // Keep track of whether or not information has been added
+    bool infoAdded = false;
+
+    // Use bufferName() for QueryBufferItem, nickName() for IrcUserItem
+    tooltip << "<p class='bold' align='center'>" << NetworkItem::escapeHTML(nickName(), true);
     if (_ircUser->userModes() != "") {
-        //TODO: Translate user Modes and add them to the table below
+        //TODO: Translate user Modes and add them to the table below and in QueryBufferItem::toolTip
         tooltip << " (" << _ircUser->userModes() << ")";
     }
     tooltip << "</p>";
 
-    auto addRow = [&](const QString& key, const QString& value, bool condition = true) {
+    auto addRow = [&](const QString& key, const QString& value, bool condition) {
         if (condition)
         {
             tooltip << "<tr><td class='bold' align='right'>" << key << "</td><td>" << value << "</td></tr>";
+            infoAdded = true;
         }
     };
 
     tooltip << "<table cellspacing='5' cellpadding='0'>";
+    addRow(tr("Modes"),
+           NetworkItem::escapeHTML(channelModes()),
+           !channelModes().isEmpty());
     if (_ircUser->isAway()) {
-        QString awayMessage(tr("(unknown)"));
-        if(!_ircUser->awayMessage().isEmpty()) {
-            awayMessage = _ircUser->awayMessage();
+        QString awayMessageHTML = QString("<p class='italic'>%1</p>").arg(tr("Unknown"));
+
+        // If away message is known, replace with the escaped message.
+        if (!_ircUser->awayMessage().isEmpty()) {
+            awayMessageHTML = NetworkItem::escapeHTML(_ircUser->awayMessage());
         }
-        addRow(tr("Away&nbsp;Message"), awayMessage);
-    }
-    addRow(tr("Realname"), _ircUser->realName(), !_ircUser->realName().isEmpty());
-    addRow(tr("Operator"), _ircUser->ircOperator(), !_ircUser->ircOperator().isEmpty());
-    addRow(tr("Suser&nbsp;Host"), _ircUser->suserHost(),!_ircUser->suserHost().isEmpty());
-    addRow(tr("Whois&nbsp;Service&nbsp;Reply"), _ircUser->whoisServiceReply(), !_ircUser->whoisServiceReply().isEmpty());
-    addRow(tr("Hostmask"), _ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!")+1));
-    addRow(tr("Operator"), _ircUser->ircOperator(), !_ircUser->ircOperator().isEmpty());
+        addRow(NetworkItem::escapeHTML(tr("Away message"), true), awayMessageHTML, true);
+    }
+    addRow(tr("Realname"),
+           NetworkItem::escapeHTML(_ircUser->realName()),
+           !_ircUser->realName().isEmpty());
+
+    // suserHost may return "<nick> is available for help", which should be translated.
+    // See https://www.alien.net.au/irc/irc2numerics.html
+    if(_ircUser->suserHost().endsWith("available for help")) {
+        addRow(NetworkItem::escapeHTML(tr("Help status"), true),
+               NetworkItem::escapeHTML(tr("Available for help")),
+               true);
+    } else {
+        addRow(NetworkItem::escapeHTML(tr("Service status"), true),
+               NetworkItem::escapeHTML(_ircUser->suserHost()),
+               !_ircUser->suserHost().isEmpty());
+    }
+
+    // Keep track of whether or not the account information's been added.  Don't show it twice.
+    bool accountAdded = false;
+    if(!_ircUser->account().isEmpty()) {
+        // IRCv3 account-notify is supported by the core and IRC server.
+        // Assume logged out (seems to be more common)
+        QString accountHTML = QString("<p class='italic'>%1</p>").arg(tr("Not logged in"));
+
+        // If account is logged in, replace with the escaped account name.
+        if (_ircUser->account() != "*") {
+            accountHTML = NetworkItem::escapeHTML(_ircUser->account());
+        }
+        addRow(NetworkItem::escapeHTML(tr("Account"), true),
+               accountHTML,
+               true);
+        // Mark the row as added
+        accountAdded = true;
+    }
+    // whoisServiceReply may return "<nick> is identified for this nick", which should be translated.
+    // See https://www.alien.net.au/irc/irc2numerics.html
+    if(_ircUser->whoisServiceReply().endsWith("identified for this nick")) {
+        addRow(NetworkItem::escapeHTML(tr("Account"), true),
+               NetworkItem::escapeHTML(tr("Identified for this nick")),
+               !accountAdded);
+        // Don't add the account row again if information's already added via account-notify
+        // Not used further down...
+        // accountAdded = true;
+    } else {
+        addRow(NetworkItem::escapeHTML(tr("Service Reply"), true),
+               NetworkItem::escapeHTML(_ircUser->whoisServiceReply()),
+               !_ircUser->whoisServiceReply().isEmpty());
+    }
+    addRow(tr("Hostmask"),
+           NetworkItem::escapeHTML(_ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!") + 1)),
+           !(_ircUser->hostmask().remove(0, _ircUser->hostmask().indexOf("!") + 1) == "@"));
+    // ircOperator may contain "is an" or "is a", which should be removed.
+    addRow(tr("Operator"),
+           NetworkItem::escapeHTML(_ircUser->ircOperator().replace("is an ", "").replace("is a ", "")),
+           !_ircUser->ircOperator().isEmpty());
 
     if (_ircUser->idleTime().isValid()) {
         QDateTime now = QDateTime::currentDateTime();
         QDateTime idle = _ircUser->idleTime();
         int idleTime = idle.secsTo(now);
-        addRow(tr("Idling&nbsp;since"), secondsToString(idleTime));
+        addRow(NetworkItem::escapeHTML(tr("Idling since"), true), secondsToString(idleTime), true);
     }
 
     if (_ircUser->loginTime().isValid()) {
-        addRow(tr("Login&nbsp;time"), _ircUser->loginTime().toString());
+        addRow(NetworkItem::escapeHTML(tr("Login time"), true), _ircUser->loginTime().toString(), true);
     }
 
-    addRow(tr("Server"), _ircUser->server(), !_ircUser->server().isEmpty());
+    addRow(tr("Server"), NetworkItem::escapeHTML(_ircUser->server()), !_ircUser->server().isEmpty());
+    tooltip << "</table>";
 
-    tooltip << "</table></qt>";
+    // If no further information found, offer an explanatory message
+    if (!infoAdded)
+        tooltip << "<p class='italic' align='center'>" << tr("No information available") << "</p>";
+
+    tooltip << "</qt>";
     return strTooltip;
 }
 
+QString IrcUserItem::channelModes() const
+{
+    // IrcUserItems are parented to UserCategoryItem, which are parented to ChannelBufferItem.
+    // We want the channel buffer item in order to get the channel-specific user modes.
+    UserCategoryItem *category = qobject_cast<UserCategoryItem *>(parent());
+    if (!category)
+        return QString();
+
+    ChannelBufferItem *channel = qobject_cast<ChannelBufferItem *>(category->parent());
+    if (!channel)
+        return QString();
+
+    return channel->nickChannelModes(nickName());
+}
+
 
 /*****************************************
  * NetworkModel
@@ -1033,7 +1318,7 @@ bool NetworkModel::isBufferIndex(const QModelIndex &index) const
 
 int NetworkModel::networkRow(NetworkId networkId) const
 {
-    NetworkItem *netItem = 0;
+    NetworkItem *netItem = nullptr;
     for (int i = 0; i < rootItem->childCount(); i++) {
         netItem = qobject_cast<NetworkItem *>(rootItem->child(i));
         if (!netItem)
@@ -1059,7 +1344,7 @@ NetworkItem *NetworkModel::findNetworkItem(NetworkId networkId) const
 {
     int netRow = networkRow(networkId);
     if (netRow == -1)
-        return 0;
+        return nullptr;
     else
         return qobject_cast<NetworkItem *>(rootItem->child(netRow));
 }
@@ -1069,7 +1354,7 @@ NetworkItem *NetworkModel::networkItem(NetworkId networkId)
 {
     NetworkItem *netItem = findNetworkItem(networkId);
 
-    if (netItem == 0) {
+    if (netItem == nullptr) {
         netItem = new NetworkItem(networkId, rootItem);
         rootItem->newChild(netItem);
     }
@@ -1100,7 +1385,7 @@ BufferItem *NetworkModel::findBufferItem(BufferId bufferId) const
     if (_bufferItemCache.contains(bufferId))
         return _bufferItemCache[bufferId];
     else
-        return 0;
+        return nullptr;
 }
 
 
@@ -1279,7 +1564,7 @@ void NetworkModel::updateBufferActivity(Message &msg)
     case Message::Action:
         if (bufferType(msg.bufferId()) == BufferInfo::ChannelBuffer) {
             const Network *net = Client::network(msg.bufferInfo().networkId());
-            IrcUser *user = net ? net->ircUser(nickFromMask(msg.sender())) : 0;
+            IrcUser *user = net ? net->ircUser(nickFromMask(msg.sender())) : nullptr;
             if (user)
                 user->setLastChannelActivity(msg.bufferId(), msg.timestamp());
         }
@@ -1343,7 +1628,7 @@ const Network *NetworkModel::networkByIndex(const QModelIndex &index) const
 {
     QVariant netVariant = index.data(NetworkIdRole);
     if (!netVariant.isValid())
-        return 0;
+        return nullptr;
 
     NetworkId networkId = netVariant.value<NetworkId>();
     return Client::network(networkId);
@@ -1486,3 +1771,24 @@ void NetworkModel::messageRedirectionSettingsChanged()
     _serverNoticesTarget = bufferSettings.serverNoticesTarget();
     _errorMsgsTarget = bufferSettings.errorMsgsTarget();
 }
+
+void NetworkModel::bufferActivityChanged(BufferId bufferId, const Message::Types activity) {
+    auto _bufferItem = findBufferItem(bufferId);
+    if (!_bufferItem) {
+        qDebug() << "NetworkModel::bufferActivityChanged(): buffer is unknown:" << bufferId;
+        return;
+    }
+    auto hiddenTypes = BufferSettings(bufferId).messageFilter();
+    auto visibleTypes = ~hiddenTypes;
+    auto activityVisibleTypesIntersection = activity & visibleTypes;
+    _bufferItem->setActivity(activityVisibleTypesIntersection, false);
+}
+
+void NetworkModel::highlightCountChanged(BufferId bufferId, int count) {
+    auto _bufferItem = findBufferItem(bufferId);
+    if (!_bufferItem) {
+        qDebug() << "NetworkModel::highlightCountChanged(): buffer is unknown:" << bufferId;
+        return;
+    }
+    _bufferItem->addActivity(Message::Types{}, count > 0);
+}