From ba06b101352fe4a3e34b88d10d5d06a4b273f031 Mon Sep 17 00:00:00 2001 From: Daniel <24928223+dnsge@users.noreply.github.com> Date: Sat, 18 Jul 2020 10:03:51 -0400 Subject: [PATCH] Open usercard on mention click (#1674) --- CHANGELOG.md | 1 + src/common/Channel.cpp | 5 +- src/common/UsernameSet.cpp | 5 + src/common/UsernameSet.hpp | 2 + src/messages/MessageBuilder.cpp | 138 ++++++++++++------ src/messages/MessageBuilder.hpp | 12 +- src/providers/twitch/IrcMessageHandler.cpp | 20 ++- src/providers/twitch/TwitchMessageBuilder.cpp | 58 +++++--- src/singletons/Settings.hpp | 2 + src/widgets/settingspages/GeneralPage.cpp | 2 + 10 files changed, 175 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4b4ee95..6825bc08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Major: We now support image thumbnails coming from the link resolver. This feature is off by default and can be enabled in the settings with the "Show link thumbnail" setting. This feature also requires the "Show link info when hovering" setting to be enabled (#1664) - Major: Added image upload functionality to i.nuuls.com with an ability to change upload destination. This works by dragging and dropping an image into a split, or pasting an image into the text edit field. (#1332, #1741) +- Minor: Clicking on @mentions will open the User Popup. (#1674) - Minor: You can now open the Twitch User Card by middle-mouse clicking a username. (#1669) - Minor: User Popup now also includes recent user messages (#1729) - Minor: BetterTTV / FrankerFaceZ emote tooltips now also have emote authors' name (#1721) diff --git a/src/common/Channel.cpp b/src/common/Channel.cpp index 45280662..3ff35385 100644 --- a/src/common/Channel.cpp +++ b/src/common/Channel.cpp @@ -148,9 +148,8 @@ void Channel::addOrReplaceTimeout(MessagePtr message) int count = s->count + 1; - MessageBuilder replacement(systemMessage, - message->searchText + QString(" (") + - QString::number(count) + " times)"); + MessageBuilder replacement(timeoutMessage, message->searchText, + count); replacement->timeoutUser = message->timeoutUser; replacement->count = count; diff --git a/src/common/UsernameSet.cpp b/src/common/UsernameSet.cpp index c9f9a031..958a6fc3 100644 --- a/src/common/UsernameSet.cpp +++ b/src/common/UsernameSet.cpp @@ -63,6 +63,11 @@ void UsernameSet::insertPrefix(const QString &value) string = value; } +bool UsernameSet::contains(const QString &value) const +{ + return this->items.count(value) == 1; +} + // // Range // diff --git a/src/common/UsernameSet.hpp b/src/common/UsernameSet.hpp index 9a24d239..f43916cc 100644 --- a/src/common/UsernameSet.hpp +++ b/src/common/UsernameSet.hpp @@ -76,6 +76,8 @@ public: std::pair insert(const QString &value); std::pair insert(QString &&value); + bool contains(const QString &value) const; + private: void insertPrefix(const QString &string); diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index e877161c..545d8735 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -24,6 +24,11 @@ MessagePtr makeSystemMessage(const QString &text) return MessageBuilder(systemMessage, text).release(); } +MessagePtr makeSystemMessage(const QString &text, const QTime &time) +{ + return MessageBuilder(systemMessage, text, time).release(); +} + std::pair makeAutomodMessage( const AutomodAction &action) { @@ -93,10 +98,11 @@ MessageBuilder::MessageBuilder() { } -MessageBuilder::MessageBuilder(SystemMessageTag, const QString &text) +MessageBuilder::MessageBuilder(SystemMessageTag, const QString &text, + const QTime &time) : MessageBuilder() { - this->emplace(); + this->emplace(time); // check system message for links // (e.g. needed for sub ticket message in sub only mode) @@ -120,17 +126,40 @@ MessageBuilder::MessageBuilder(SystemMessageTag, const QString &text) this->message().searchText = text; } +MessageBuilder::MessageBuilder(TimeoutMessageTag, + const QString &systemMessageText, int times) + : MessageBuilder() +{ + QString username = systemMessageText.split(" ").at(0); + QString remainder = systemMessageText.mid(username.length() + 1); + + QString text; + + this->emplace(); + this->emplaceSystemTextAndUpdate(username, text) + ->setLink({Link::UserInfo, username}); + this->emplaceSystemTextAndUpdate( + QString("%1 (%2 times)").arg(remainder.trimmed()).arg(times), text); + + this->message().messageText = text; + this->message().searchText = text; +} + MessageBuilder::MessageBuilder(TimeoutMessageTag, const QString &username, const QString &durationInSeconds, const QString &reason, bool multipleTimes) : MessageBuilder() { + QString fullText; QString text; - text.append(username); + this->emplace(); + this->emplaceSystemTextAndUpdate(username, fullText) + ->setLink({Link::UserInfo, username}); + if (!durationInSeconds.isEmpty()) { - text.append(" has been timed out"); + text.append("has been timed out"); // TODO: Implement who timed the user out @@ -144,7 +173,7 @@ MessageBuilder::MessageBuilder(TimeoutMessageTag, const QString &username, } else { - text.append(" has been permanently banned"); + text.append("has been permanently banned"); } if (reason.length() > 0) @@ -164,11 +193,10 @@ MessageBuilder::MessageBuilder(TimeoutMessageTag, const QString &username, this->message().flags.set(MessageFlag::Timeout); this->message().flags.set(MessageFlag::DoNotTriggerNotification); this->message().timeoutUser = username; - this->emplace(); - this->emplace(text, MessageElementFlag::Text, - MessageColor::System); - this->message().messageText = text; - this->message().searchText = text; + + this->emplaceSystemTextAndUpdate(text, fullText); + this->message().messageText = fullText; + this->message().searchText = fullText; } // XXX: This does not belong in the MessageBuilder, this should be part of the TwitchMessageBuilder @@ -187,77 +215,82 @@ MessageBuilder::MessageBuilder(const BanAction &action, uint32_t count) if (action.target.id == current->getUserId()) { - text.append("You were "); + this->emplaceSystemTextAndUpdate("You were", text); if (action.isBan()) { - text.append("banned"); + this->emplaceSystemTextAndUpdate("banned", text); } else { - text.append( - QString("timed out for %1").arg(formatTime(action.duration))); + this->emplaceSystemTextAndUpdate( + QString("timed out for %1").arg(formatTime(action.duration)), + text); } if (!action.source.name.isEmpty()) { - text.append(" by "); - text.append(action.source.name); + this->emplaceSystemTextAndUpdate("by", text); + this->emplaceSystemTextAndUpdate( + action.source.name + (action.reason.isEmpty() ? "." : ":"), + text) + ->setLink({Link::UserInfo, action.source.name}); } - if (action.reason.isEmpty()) + if (!action.reason.isEmpty()) { - text.append("."); - } - else - { - text.append(QString(": \"%1\".").arg(action.reason)); + this->emplaceSystemTextAndUpdate( + QString("\"%1\".").arg(action.reason), text); } } else { if (action.isBan()) { + this->emplaceSystemTextAndUpdate(action.source.name, text) + ->setLink({Link::UserInfo, action.source.name}); + this->emplaceSystemTextAndUpdate("banned", text); if (action.reason.isEmpty()) { - text = QString("%1 banned %2.") // - .arg(action.source.name) - .arg(action.target.name); + this->emplaceSystemTextAndUpdate(action.target.name, text) + ->setLink({Link::UserInfo, action.target.name}); } else { - text = QString("%1 banned %2: \"%3\".") // - .arg(action.source.name) - .arg(action.target.name) - .arg(action.reason); + this->emplaceSystemTextAndUpdate(action.target.name + ":", text) + ->setLink({Link::UserInfo, action.target.name}); + this->emplaceSystemTextAndUpdate( + QString("\"%1\".").arg(action.reason), text); } } else { + this->emplaceSystemTextAndUpdate(action.source.name, text) + ->setLink({Link::UserInfo, action.source.name}); + this->emplaceSystemTextAndUpdate("timed out", text); + this->emplaceSystemTextAndUpdate(action.target.name, text) + ->setLink({Link::UserInfo, action.target.name}); if (action.reason.isEmpty()) { - text = QString("%1 timed out %2 for %3.") // - .arg(action.source.name) - .arg(action.target.name) - .arg(formatTime(action.duration)); + this->emplaceSystemTextAndUpdate( + QString("for %1.").arg(formatTime(action.duration)), text); } else { - text = QString("%1 timed out %2 for %3: \"%4\".") // - .arg(action.source.name) - .arg(action.target.name) - .arg(formatTime(action.duration)) - .arg(action.reason); + this->emplaceSystemTextAndUpdate( + QString("for %1: \"%2\".") + .arg(formatTime(action.duration)) + .arg(action.reason), + text); } if (count > 1) { - text.append(QString(" (%1 times)").arg(count)); + this->emplaceSystemTextAndUpdate( + QString("(%1 times)").arg(count), text); } } } - this->emplace(text, MessageElementFlag::Text, - MessageColor::System); this->message().messageText = text; this->message().searchText = text; } @@ -271,14 +304,15 @@ MessageBuilder::MessageBuilder(const UnbanAction &action) this->message().timeoutUser = action.target.name; - QString text = - QString("%1 %2 %3.") - .arg(action.source.name) - .arg(QString(action.wasBan() ? "unbanned" : "untimedout")) - .arg(action.target.name); + QString text; + + this->emplaceSystemTextAndUpdate(action.source.name, text) + ->setLink({Link::UserInfo, action.source.name}); + this->emplaceSystemTextAndUpdate( + action.wasBan() ? "unbanned" : "untimedout", text); + this->emplaceSystemTextAndUpdate(action.target.name, text) + ->setLink({Link::UserInfo, action.target.name}); - this->emplace(text, MessageElementFlag::Text, - MessageColor::System); this->message().messageText = text; this->message().searchText = text; } @@ -446,4 +480,12 @@ void MessageBuilder::addLink(const QString &origLink, }); } +TextElement *MessageBuilder::emplaceSystemTextAndUpdate(const QString &text, + QString &toUpdate) +{ + toUpdate.append(text + " "); + return this->emplace(text, MessageElementFlag::Text, + MessageColor::System); +} + } // namespace chatterino diff --git a/src/messages/MessageBuilder.hpp b/src/messages/MessageBuilder.hpp index 92ae3d6c..9cc4c9ed 100644 --- a/src/messages/MessageBuilder.hpp +++ b/src/messages/MessageBuilder.hpp @@ -22,6 +22,7 @@ const SystemMessageTag systemMessage{}; const TimeoutMessageTag timeoutMessage{}; MessagePtr makeSystemMessage(const QString &text); +MessagePtr makeSystemMessage(const QString &text, const QTime &time); std::pair makeAutomodMessage( const AutomodAction &action); @@ -37,7 +38,10 @@ class MessageBuilder { public: MessageBuilder(); - MessageBuilder(SystemMessageTag, const QString &text); + MessageBuilder(SystemMessageTag, const QString &text, + const QTime &time = QTime::currentTime()); + MessageBuilder(TimeoutMessageTag, const QString &systemMessageText, + int times); MessageBuilder(TimeoutMessageTag, const QString &username, const QString &durationInSeconds, const QString &reason, bool multipleTimes); @@ -67,6 +71,12 @@ public: } private: + // Helper method that emplaces some text stylized as system text + // and then appends that text to the QString parameter "toUpdate". + // Returns the TextElement that was emplaced. + TextElement *emplaceSystemTextAndUpdate(const QString &text, + QString &toUpdate); + std::shared_ptr message_; }; diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 2abf3b9e..fc113a14 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -634,7 +634,25 @@ std::vector IrcMessageHandler::parseNoticeMessage( { std::vector builtMessages; - builtMessages.emplace_back(makeSystemMessage(message->content())); + if (message->tags().contains("historical")) + { + bool customReceived = false; + qint64 ts = message->tags() + .value("rm-received-ts") + .toLongLong(&customReceived); + if (!customReceived) + { + ts = message->tags().value("tmi-sent-ts").toLongLong(); + } + + QDateTime dateTime = QDateTime::fromMSecsSinceEpoch(ts); + builtMessages.emplace_back( + makeSystemMessage(message->content(), dateTime.time())); + } + else + { + builtMessages.emplace_back(makeSystemMessage(message->content())); + } return builtMessages; } diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index 10979d97..a5475c71 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -27,6 +27,9 @@ namespace { +// matches a mention with punctuation at the end, like "@username," or "@username!!!" where capture group would return "username" +const QRegularExpression mentionRegex("^@(\\w+)[.,!?;]*?$"); + const QSet zeroWidthEmotes{ "SoSnowy", "IceCold", "SantaHat", "TopHat", "ReinDeer", "CandyCane", "cvMask", "cvHazmat", @@ -407,29 +410,50 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_) // Actually just text auto linkString = this->matchLink(string); - auto link = Link(); auto textColor = this->action_ ? MessageColor(this->usernameColor_) : MessageColor(MessageColor::Text); - if (linkString.isEmpty()) - { - if (string.startsWith('@')) - { - this->emplace(string, MessageElementFlag::BoldUsername, - textColor, FontStyle::ChatMediumBold); - this->emplace( - string, MessageElementFlag::NonBoldUsername, textColor); - } - else - { - this->emplace(string, MessageElementFlag::Text, - textColor); - } - } - else + if (!linkString.isEmpty()) { this->addLink(string, linkString); + return; } + + if (string.startsWith('@')) + { + auto match = mentionRegex.match(string); + // Only treat as @mention if valid username + if (match.hasMatch()) + { + QString username = match.captured(1); + this->emplace(string, MessageElementFlag::BoldUsername, + textColor, FontStyle::ChatMediumBold) + ->setLink({Link::UserInfo, username}); + + this->emplace( + string, MessageElementFlag::NonBoldUsername, textColor) + ->setLink({Link::UserInfo, username}); + return; + } + } + + if (this->twitchChannel != nullptr && getSettings()->findAllUsernames) + { + auto chatters = this->twitchChannel->accessChatters(); + if (chatters->contains(string)) + { + this->emplace(string, MessageElementFlag::BoldUsername, + textColor, FontStyle::ChatMediumBold) + ->setLink({Link::UserInfo, string}); + + this->emplace( + string, MessageElementFlag::NonBoldUsername, textColor) + ->setLink({Link::UserInfo, string}); + return; + } + } + + this->emplace(string, MessageElementFlag::Text, textColor); } void TwitchMessageBuilder::parseMessageID() diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index e0f34b9f..fea6d253 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -94,6 +94,8 @@ public: BoolSetting enableSmoothScrollingNewMessages = { "/appearance/smoothScrollingNewMessages", false}; BoolSetting boldUsernames = {"/appearance/messages/boldUsernames", false}; + BoolSetting findAllUsernames = {"/appearance/messages/findAllUsernames", + false}; // BoolSetting customizable splitheader BoolSetting headerViewerCount = {"/appearance/splitheader/showViewerCount", false}; diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index 923aa31b..02bfae09 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -511,6 +511,8 @@ void GeneralPage::initLayout(SettingsLayout &layout) layout.addCheckbox("Show parted users (< 1000 chatters)", s.showParts); layout.addCheckbox("Lowercase domains (anti-phishing)", s.lowercaseDomains); layout.addCheckbox("Bold @usernames", s.boldUsernames); + layout.addCheckbox("Try to find usernames without @ prefix", + s.findAllUsernames); layout.addDropdown( "Username font weight", {"50", "Default", "75", "100"}, s.boldScale, [](auto val) {