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: 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) - 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: 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: User Popup now also includes recent user messages (#1729)
- Minor: BetterTTV / FrankerFaceZ emote tooltips now also have emote authors' name (#1721) - 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; int count = s->count + 1;
MessageBuilder replacement(systemMessage, MessageBuilder replacement(timeoutMessage, message->searchText,
message->searchText + QString(" (") + count);
QString::number(count) + " times)");
replacement->timeoutUser = message->timeoutUser; replacement->timeoutUser = message->timeoutUser;
replacement->count = count; replacement->count = count;

View file

@ -63,6 +63,11 @@ void UsernameSet::insertPrefix(const QString &value)
string = value; string = value;
} }
bool UsernameSet::contains(const QString &value) const
{
return this->items.count(value) == 1;
}
// //
// Range // Range
// //

View file

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

View file

@ -24,6 +24,11 @@ MessagePtr makeSystemMessage(const QString &text)
return MessageBuilder(systemMessage, text).release(); return MessageBuilder(systemMessage, text).release();
} }
MessagePtr makeSystemMessage(const QString &text, const QTime &time)
{
return MessageBuilder(systemMessage, text, time).release();
}
std::pair<MessagePtr, MessagePtr> makeAutomodMessage( std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
const AutomodAction &action) 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() : MessageBuilder()
{ {
this->emplace<TimestampElement>(); this->emplace<TimestampElement>(time);
// check system message for links // check system message for links
// (e.g. needed for sub ticket message in sub only mode) // (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; 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, MessageBuilder::MessageBuilder(TimeoutMessageTag, const QString &username,
const QString &durationInSeconds, const QString &durationInSeconds,
const QString &reason, bool multipleTimes) const QString &reason, bool multipleTimes)
: MessageBuilder() : MessageBuilder()
{ {
QString fullText;
QString text; QString text;
text.append(username); this->emplace<TimestampElement>();
this->emplaceSystemTextAndUpdate(username, fullText)
->setLink({Link::UserInfo, username});
if (!durationInSeconds.isEmpty()) if (!durationInSeconds.isEmpty())
{ {
text.append(" has been timed out"); text.append("has been timed out");
// TODO: Implement who timed the user out // TODO: Implement who timed the user out
@ -144,7 +173,7 @@ MessageBuilder::MessageBuilder(TimeoutMessageTag, const QString &username,
} }
else else
{ {
text.append(" has been permanently banned"); text.append("has been permanently banned");
} }
if (reason.length() > 0) 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::Timeout);
this->message().flags.set(MessageFlag::DoNotTriggerNotification); this->message().flags.set(MessageFlag::DoNotTriggerNotification);
this->message().timeoutUser = username; this->message().timeoutUser = username;
this->emplace<TimestampElement>();
this->emplace<TextElement>(text, MessageElementFlag::Text, this->emplaceSystemTextAndUpdate(text, fullText);
MessageColor::System); this->message().messageText = fullText;
this->message().messageText = text; this->message().searchText = fullText;
this->message().searchText = text;
} }
// XXX: This does not belong in the MessageBuilder, this should be part of the TwitchMessageBuilder // 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()) if (action.target.id == current->getUserId())
{ {
text.append("You were "); this->emplaceSystemTextAndUpdate("You were", text);
if (action.isBan()) if (action.isBan())
{ {
text.append("banned"); this->emplaceSystemTextAndUpdate("banned", text);
} }
else else
{ {
text.append( this->emplaceSystemTextAndUpdate(
QString("timed out for %1").arg(formatTime(action.duration))); QString("timed out for %1").arg(formatTime(action.duration)),
text);
} }
if (!action.source.name.isEmpty()) if (!action.source.name.isEmpty())
{ {
text.append(" by "); this->emplaceSystemTextAndUpdate("by", text);
text.append(action.source.name); 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("."); this->emplaceSystemTextAndUpdate(
} QString("\"%1\".").arg(action.reason), text);
else
{
text.append(QString(": \"%1\".").arg(action.reason));
} }
} }
else else
{ {
if (action.isBan()) if (action.isBan())
{ {
this->emplaceSystemTextAndUpdate(action.source.name, text)
->setLink({Link::UserInfo, action.source.name});
this->emplaceSystemTextAndUpdate("banned", text);
if (action.reason.isEmpty()) if (action.reason.isEmpty())
{ {
text = QString("%1 banned %2.") // this->emplaceSystemTextAndUpdate(action.target.name, text)
.arg(action.source.name) ->setLink({Link::UserInfo, action.target.name});
.arg(action.target.name);
} }
else else
{ {
text = QString("%1 banned %2: \"%3\".") // this->emplaceSystemTextAndUpdate(action.target.name + ":", text)
.arg(action.source.name) ->setLink({Link::UserInfo, action.target.name});
.arg(action.target.name) this->emplaceSystemTextAndUpdate(
.arg(action.reason); QString("\"%1\".").arg(action.reason), text);
} }
} }
else 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()) if (action.reason.isEmpty())
{ {
text = QString("%1 timed out %2 for %3.") // this->emplaceSystemTextAndUpdate(
.arg(action.source.name) QString("for %1.").arg(formatTime(action.duration)), text);
.arg(action.target.name)
.arg(formatTime(action.duration));
} }
else else
{ {
text = QString("%1 timed out %2 for %3: \"%4\".") // this->emplaceSystemTextAndUpdate(
.arg(action.source.name) QString("for %1: \"%2\".")
.arg(action.target.name) .arg(formatTime(action.duration))
.arg(formatTime(action.duration)) .arg(action.reason),
.arg(action.reason); text);
} }
if (count > 1) 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().messageText = text;
this->message().searchText = text; this->message().searchText = text;
} }
@ -271,14 +304,15 @@ MessageBuilder::MessageBuilder(const UnbanAction &action)
this->message().timeoutUser = action.target.name; this->message().timeoutUser = action.target.name;
QString text = QString text;
QString("%1 %2 %3.")
.arg(action.source.name) this->emplaceSystemTextAndUpdate(action.source.name, text)
.arg(QString(action.wasBan() ? "unbanned" : "untimedout")) ->setLink({Link::UserInfo, action.source.name});
.arg(action.target.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().messageText = text;
this->message().searchText = 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 } // namespace chatterino

View file

@ -22,6 +22,7 @@ const SystemMessageTag systemMessage{};
const TimeoutMessageTag timeoutMessage{}; const TimeoutMessageTag timeoutMessage{};
MessagePtr makeSystemMessage(const QString &text); MessagePtr makeSystemMessage(const QString &text);
MessagePtr makeSystemMessage(const QString &text, const QTime &time);
std::pair<MessagePtr, MessagePtr> makeAutomodMessage( std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
const AutomodAction &action); const AutomodAction &action);
@ -37,7 +38,10 @@ class MessageBuilder
{ {
public: public:
MessageBuilder(); 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, MessageBuilder(TimeoutMessageTag, const QString &username,
const QString &durationInSeconds, const QString &reason, const QString &durationInSeconds, const QString &reason,
bool multipleTimes); bool multipleTimes);
@ -67,6 +71,12 @@ public:
} }
private: 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_; std::shared_ptr<Message> message_;
}; };

View file

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

View file

@ -27,6 +27,9 @@
namespace { 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{ const QSet<QString> zeroWidthEmotes{
"SoSnowy", "IceCold", "SantaHat", "TopHat", "SoSnowy", "IceCold", "SantaHat", "TopHat",
"ReinDeer", "CandyCane", "cvMask", "cvHazmat", "ReinDeer", "CandyCane", "cvMask", "cvHazmat",
@ -407,29 +410,50 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_)
// Actually just text // Actually just text
auto linkString = this->matchLink(string); auto linkString = this->matchLink(string);
auto link = Link();
auto textColor = this->action_ ? MessageColor(this->usernameColor_) auto textColor = this->action_ ? MessageColor(this->usernameColor_)
: MessageColor(MessageColor::Text); : MessageColor(MessageColor::Text);
if (linkString.isEmpty()) 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
{ {
this->addLink(string, linkString); 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() void TwitchMessageBuilder::parseMessageID()

View file

@ -94,6 +94,8 @@ public:
BoolSetting enableSmoothScrollingNewMessages = { BoolSetting enableSmoothScrollingNewMessages = {
"/appearance/smoothScrollingNewMessages", false}; "/appearance/smoothScrollingNewMessages", false};
BoolSetting boldUsernames = {"/appearance/messages/boldUsernames", false}; BoolSetting boldUsernames = {"/appearance/messages/boldUsernames", false};
BoolSetting findAllUsernames = {"/appearance/messages/findAllUsernames",
false};
// BoolSetting customizable splitheader // BoolSetting customizable splitheader
BoolSetting headerViewerCount = {"/appearance/splitheader/showViewerCount", BoolSetting headerViewerCount = {"/appearance/splitheader/showViewerCount",
false}; false};

View file

@ -511,6 +511,8 @@ void GeneralPage::initLayout(SettingsLayout &layout)
layout.addCheckbox("Show parted users (< 1000 chatters)", s.showParts); layout.addCheckbox("Show parted users (< 1000 chatters)", s.showParts);
layout.addCheckbox("Lowercase domains (anti-phishing)", s.lowercaseDomains); layout.addCheckbox("Lowercase domains (anti-phishing)", s.lowercaseDomains);
layout.addCheckbox("Bold @usernames", s.boldUsernames); layout.addCheckbox("Bold @usernames", s.boldUsernames);
layout.addCheckbox("Try to find usernames without @ prefix",
s.findAllUsernames);
layout.addDropdown<float>( layout.addDropdown<float>(
"Username font weight", {"50", "Default", "75", "100"}, s.boldScale, "Username font weight", {"50", "Default", "75", "100"}, s.boldScale,
[](auto val) { [](auto val) {