diff --git a/CHANGELOG.md b/CHANGELOG.md index 8575e529..cf601edd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ - Bugfix: Fixed a crash that could occur when an image started loading mid app shutdown. (#6213) - Bugfix: Fixed some minor typos. (#6196) - Bugfix: Fixed inconsistent spaces in messages when using fractional scaling. (#6231, #6254) +- Bugfix: Don't add reply buttons to messages that are invalid reply targets. (#6119) - Dev: Mini refactor of Split. (#6148) - Dev: Conan will no longer generate a `CMakeUserPresets.json` file. (#6117) - Dev: Pass `--force-openssl` when installing from CMake in Qt 6.8+. (#6129) diff --git a/src/messages/Message.cpp b/src/messages/Message.cpp index 7990500c..5d1d3ab1 100644 --- a/src/messages/Message.cpp +++ b/src/messages/Message.cpp @@ -159,4 +159,44 @@ QJsonObject Message::toJson() const return msg; } +Message::ReplyStatus Message::isReplyable() const +{ + if (this->loginName.isEmpty()) + { + // no replies can happen + return ReplyStatus::NotReplyable; + } + + constexpr int oneDayInSeconds = 24 * 60 * 60; + bool messageReplyable = true; + if (this->flags.hasAny({MessageFlag::System, MessageFlag::Subscription, + MessageFlag::Timeout, MessageFlag::Whisper, + MessageFlag::ModerationAction, + MessageFlag::InvalidReplyTarget}) || + this->serverReceivedTime.secsTo(QDateTime::currentDateTime()) > + oneDayInSeconds) + { + messageReplyable = false; + } + + if (this->replyThread != nullptr) + { + if (const auto &rootPtr = this->replyThread->root(); rootPtr != nullptr) + { + assert(this != rootPtr.get()); + if (rootPtr->isReplyable() == ReplyStatus::NotReplyable) + { + // thread parent must be replyable to be replyable + return ReplyStatus::NotReplyableDueToThread; + } + + return messageReplyable ? ReplyStatus::ReplyableWithThread + : ReplyStatus::NotReplyableWithThread; + } + } + + return messageReplyable ? ReplyStatus::Replyable + : ReplyStatus::NotReplyable; +} + } // namespace chatterino diff --git a/src/messages/Message.hpp b/src/messages/Message.hpp index 7f6af752..0762df4d 100644 --- a/src/messages/Message.hpp +++ b/src/messages/Message.hpp @@ -62,6 +62,29 @@ struct Message { // The root of the thread does not have replyThread set. std::shared_ptr replyThread; MessagePtr replyParent; + enum class ReplyStatus : std::uint8_t { + /// message has no reply thread, and message is not replyable + /// + /// e.g. due to message being deleted or too old + NotReplyable, + + /// message has no reply thread, but message is replyable + Replyable, + + /// message is part of a reply thread. both message & thread are replyable + ReplyableWithThread, + + /// message is part of a reply thread. thread is replyable, but message is not replyable + /// + /// e.g. due to message being deleted or too old + NotReplyableWithThread, + + /// message is part of a reply thread. neither reply or message is replyable + /// + /// e.g. due to message at the top of the thread being deleted + NotReplyableDueToThread, + }; + ReplyStatus isReplyable() const; uint32_t count = 1; std::vector> elements; diff --git a/src/messages/MessageFlag.hpp b/src/messages/MessageFlag.hpp index 48eacaf6..39702b81 100644 --- a/src/messages/MessageFlag.hpp +++ b/src/messages/MessageFlag.hpp @@ -65,6 +65,12 @@ enum class MessageFlag : std::int64_t { /// - forsen added "blockedterm" as a blocked term /// - Your message is being checked by mods and has not been sent ModerationAction = (1LL << 41), + /// The message can't be replied to + /// Examples: + /// - message was deleted via single channel chat message deletion (IRC: CLEARMSG, EVENTSUB: channel.chat.message_delete) + /// - message was deleted via chat clear user messages (IRC: CLEARCHAT(user), EVENTSUB: channel.chat.clear_user_messages) + /// Note: If this message is inside a reply thread, the root must not have the flag either. + InvalidReplyTarget = (1LL << 42), }; using MessageFlags = FlagsEnum; diff --git a/src/messages/layouts/MessageLayout.cpp b/src/messages/layouts/MessageLayout.cpp index ad51b830..9807c4c7 100644 --- a/src/messages/layouts/MessageLayout.cpp +++ b/src/messages/layouts/MessageLayout.cpp @@ -544,22 +544,4 @@ void MessageLayout::addSelectionText(QString &str, uint32_t from, uint32_t to, this->container_.addSelectionText(str, from, to, copymode); } -bool MessageLayout::isReplyable() const -{ - if (this->message_->loginName.isEmpty()) - { - return false; - } - - if (this->message_->flags.hasAny( - {MessageFlag::System, MessageFlag::Subscription, - MessageFlag::Timeout, MessageFlag::Whisper, - MessageFlag::ModerationAction})) - { - return false; - } - - return true; -} - } // namespace chatterino diff --git a/src/messages/layouts/MessageLayout.hpp b/src/messages/layouts/MessageLayout.hpp index 45380d7a..c2253203 100644 --- a/src/messages/layouts/MessageLayout.hpp +++ b/src/messages/layouts/MessageLayout.hpp @@ -110,7 +110,6 @@ public: // Misc bool isDisabled() const; - bool isReplyable() const; private: // methods diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index ac57870e..6cc6b3d0 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -329,6 +329,39 @@ void IrcMessageHandler::parseMessageInto(Communi::IrcMessage *message, sink.addOrReplaceTimeout(std::move(clearChat.message), time); } } + + if (command == u"CLEARMSG"_s) + { + // check parameter count + if (message->parameters().length() < 1) + { + return; + } + + QString chanName; + if (!trimChannelName(message->parameter(0), chanName)) + { + return; + } + + auto tags = message->tags(); + + QString targetID = tags.value("target-msg-id").toString(); + + auto msg = sink.findMessageByID(targetID); + if (msg == nullptr) + { + return; + } + + msg->flags.set(MessageFlag::Disabled); + msg->flags.set(MessageFlag::InvalidReplyTarget); + if (!getSettings()->hideDeletionActions) + { + sink.addMessage(MessageBuilder::makeDeletionMessageFromIRC(msg), + MessageContext::Original); + } + } } void IrcMessageHandler::handlePrivMessage(Communi::IrcPrivateMessage *message, @@ -519,6 +552,7 @@ void IrcMessageHandler::handleClearMessageMessage(Communi::IrcMessage *message) } msg->flags.set(MessageFlag::Disabled); + msg->flags.set(MessageFlag::InvalidReplyTarget); if (!getSettings()->hideDeletionActions) { chan->addMessage(MessageBuilder::makeDeletionMessageFromIRC(msg), diff --git a/src/util/ChannelHelpers.hpp b/src/util/ChannelHelpers.hpp index 7a9436f3..2f5b9d21 100644 --- a/src/util/ChannelHelpers.hpp +++ b/src/util/ChannelHelpers.hpp @@ -120,6 +120,7 @@ void addOrReplaceChannelTimeout(const Buf &buffer, MessagePtr message, // FOURTF: disabled for now // PAJLADA: Shitty solution described in Message.hpp s->flags.set(MessageFlag::Disabled); + s->flags.set(MessageFlag::InvalidReplyTarget); } } } diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index ef948e29..a27adad8 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -2615,19 +2615,72 @@ void ChannelView::addMessageContextMenuItems(QMenu *menu, }); // Only display reply option where it makes sense - if (this->canReplyToMessages() && layout->isReplyable()) + if (this->canReplyToMessages()) { const auto &messagePtr = layout->getMessagePtr(); - menu->addAction("&Reply to message", [this, &messagePtr] { - this->setInputReply(messagePtr); - }); - - if (messagePtr->replyThread != nullptr) + switch (messagePtr->isReplyable()) { - menu->addAction("Reply to &original thread", [this, &messagePtr] { - this->setInputReply(messagePtr->replyThread->root()); - }); + case Message::ReplyStatus::Replyable: { + menu->addAction("&Reply to message", [this, &messagePtr] { + this->setInputReply(messagePtr); + }); + break; + } + case Message::ReplyStatus::NotReplyable: { + const auto &replyAction = + menu->addAction("&Reply to message", [this, &messagePtr] { + this->setInputReply(messagePtr); + }); + replyAction->setEnabled(false); + break; + } + + case Message::ReplyStatus::ReplyableWithThread: { + menu->addAction("&Reply to message", [this, &messagePtr] { + this->setInputReply(messagePtr); + }); + menu->addAction( + "Reply to &original thread", [this, &messagePtr] { + this->setInputReply(messagePtr->replyThread->root()); + }); + break; + } + + case Message::ReplyStatus::NotReplyableWithThread: { + const auto &replyAction = + menu->addAction("&Reply to message", [this, &messagePtr] { + this->setInputReply(messagePtr); + }); + replyAction->setEnabled(false); + + menu->addAction( + "Reply to &original thread", [this, &messagePtr] { + this->setInputReply(messagePtr->replyThread->root()); + }); + break; + } + + case Message::ReplyStatus::NotReplyableDueToThread: { + const auto &replyAction = + menu->addAction("&Reply to message", [this, &messagePtr] { + this->setInputReply(messagePtr); + }); + replyAction->setEnabled(false); + + const auto &replyThreadAction = menu->addAction( + "Reply to &original thread", [this, &messagePtr] { + this->setInputReply(messagePtr->replyThread->root()); + }); + + replyThreadAction->setEnabled(false); + break; + } + } + + if (const auto &messagePtr = layout->getMessagePtr(); + messagePtr->replyThread != nullptr) + { menu->addAction("View &thread", [this, &messagePtr] { this->showReplyThreadPopup(messagePtr); }); @@ -3057,7 +3110,11 @@ void ChannelView::handleLinkClick(QMouseEvent *event, const Link &link, } break; case Link::ReplyToMessage: { - this->setInputReply(layout->getMessagePtr()); + if (layout->getMessagePtr()->isReplyable() != + Message::ReplyStatus::NotReplyable) + { + this->setInputReply(layout->getMessagePtr()); + } } break; case Link::ViewThread: {