Open usercard on mention click (#1674)

This commit is contained in:
Daniel 2020-07-18 10:03:51 -04:00 committed by GitHub
parent 276f3e1d98
commit ba06b10135
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 175 additions and 70 deletions

View file

@ -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)

View file

@ -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;

View file

@ -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
//

View file

@ -76,6 +76,8 @@ public:
std::pair<Iterator, bool> insert(const QString &value);
std::pair<Iterator, bool> insert(QString &&value);
bool contains(const QString &value) const;
private:
void insertPrefix(const QString &string);

View file

@ -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<MessagePtr, MessagePtr> 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<TimestampElement>();
this->emplace<TimestampElement>(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<TimestampElement>();
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<TimestampElement>();
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<TimestampElement>();
this->emplace<TextElement>(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<TextElement>(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<TextElement>(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<TextElement>(text, MessageElementFlag::Text,
MessageColor::System);
}
} // namespace chatterino

View file

@ -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<MessagePtr, MessagePtr> 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> message_;
};

View file

@ -634,7 +634,25 @@ std::vector<MessagePtr> IrcMessageHandler::parseNoticeMessage(
{
std::vector<MessagePtr> 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;
}

View file

@ -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<QString> 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<TextElement>(string, MessageElementFlag::BoldUsername,
textColor, FontStyle::ChatMediumBold);
this->emplace<TextElement>(
string, MessageElementFlag::NonBoldUsername, textColor);
}
else
{
this->emplace<TextElement>(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<TextElement>(string, MessageElementFlag::BoldUsername,
textColor, FontStyle::ChatMediumBold)
->setLink({Link::UserInfo, username});
this->emplace<TextElement>(
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<TextElement>(string, MessageElementFlag::BoldUsername,
textColor, FontStyle::ChatMediumBold)
->setLink({Link::UserInfo, string});
this->emplace<TextElement>(
string, MessageElementFlag::NonBoldUsername, textColor)
->setLink({Link::UserInfo, string});
return;
}
}
this->emplace<TextElement>(string, MessageElementFlag::Text, textColor);
}
void TwitchMessageBuilder::parseMessageID()

View file

@ -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};

View file

@ -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<float>(
"Username font weight", {"50", "Default", "75", "100"}, s.boldScale,
[](auto val) {