diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9debd28cdbe..d1c0519118c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,9 +19,7 @@ env: C2_ENABLE_LTO: ${{ github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/bugfix-release/') || startsWith(github.ref, 'refs/heads/release/') }} CHATTERINO_REQUIRE_CLEAN_GIT: On C2_BUILD_WITH_QT6: Off - # Last known good conan version - # 2.0.3 has a bug on Windows (conan-io/conan#13606) - CONAN_VERSION: 2.0.2 + CONAN_VERSION: 2.11.0 jobs: build-ubuntu-docker: diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 40c03c3874a..52d844a2b09 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -10,9 +10,7 @@ env: TWITCH_PUBSUB_SERVER_TAG: v1.0.7 HTTPBOX_TAG: v0.2.1 QT_QPA_PLATFORM: minimal - # Last known good conan version - # 2.0.3 has a bug on Windows (conan-io/conan#13606) - CONAN_VERSION: 2.0.2 + CONAN_VERSION: 2.11.0 concurrency: group: test-windows-${{ github.ref }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b1b6bd31e59..f2c057543a9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -96,7 +96,7 @@ jobs: working-directory: build-test - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v5.0.7 + uses: codecov/codecov-action@v5.1.2 with: token: ${{ secrets.CODECOV_TOKEN }} plugins: gcov diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml index 7e8a5091a70..d2a03827991 100644 --- a/.github/workflows/winget.yml +++ b/.github/workflows/winget.yml @@ -7,7 +7,7 @@ jobs: runs-on: windows-latest if: ${{ startsWith(github.event.release.tag_name, 'v') }} steps: - - uses: vedantmgoyal2009/winget-releaser@v2 + - uses: vedantmgoyal9/winget-releaser@main with: identifier: ChatterinoTeam.Chatterino installers-regex: ^Chatterino.Installer.exe$ diff --git a/CHANGELOG.md b/CHANGELOG.md index dfebb53ecad..089d8f67066 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,15 @@ ## Unversioned +- Minor: `/clear` messages are now stacked like timeouts. (#5806) +- Minor: Treat all browsers starting with `firefox` as a Firefox browser. (#5805) +- Minor: Remove incognito browser support for `opera/launcher` (this should no longer be a thing). (#5805) +- Minor: Remove incognito browser support for `iexplore`, because internet explorer is EOL. (#5810) +- Bugfix: Fixed a crash relating to Lua HTTP. (#5800) +- Bugfix: Fixed a crash that could occur on Linux and macOS when clicking "Install" from the update prompt. (#5818) +- Bugfix: Fixed missing word wrap in update popup. (#5811) - Bugfix: Fixed tabs not scaling to the default scale when changing the scale from a non-default value. (#5794) +- Dev: Updated Conan dependencies. (#5776) ## 2.5.2 diff --git a/conanfile.py b/conanfile.py index a1d498695c8..581ec445723 100644 --- a/conanfile.py +++ b/conanfile.py @@ -1,11 +1,12 @@ from conan import ConanFile from conan.tools.files import copy +from conan.tools.cmake import CMakeToolchain from os import path class Chatterino(ConanFile): name = "Chatterino" - requires = "boost/1.83.0" + requires = "boost/1.86.0" settings = "os", "compiler", "build_type", "arch" default_options = { "with_benchmark": False, @@ -18,18 +19,29 @@ class Chatterino(ConanFile): # Qt is built with OpenSSL 3 from version 6.5.0 onwards "with_openssl3": [True, False], } - generators = "CMakeDeps", "CMakeToolchain" + generators = "CMakeDeps" def requirements(self): if self.options.get_safe("with_benchmark", False): - self.requires("benchmark/1.7.1") + self.requires("benchmark/1.9.0") if self.options.get_safe("with_openssl3", False): - self.requires("openssl/3.2.0") + self.requires("openssl/3.3.2") else: self.requires("openssl/1.1.1t") def generate(self): + tc = CMakeToolchain(self) + tc.blocks.remove("compilers") + tc.blocks.remove("cmake_flags_init") + tc.blocks.remove("cppstd") + tc.blocks.remove("libcxx") + tc.blocks.remove("generic_system") + tc.blocks.remove("user_toolchain") + tc.blocks.remove("output_dirs") + tc.blocks.remove("apple_system") + tc.generate() + def copy_bin(dep, selector, subdir): src = path.realpath(dep.cpp_info.bindirs[0]) dst = path.realpath(path.join(self.build_folder, subdir)) diff --git a/docs/make-release.md b/docs/make-release.md index 1509289fd39..d60cbe82a2f 100644 --- a/docs/make-release.md +++ b/docs/make-release.md @@ -14,12 +14,17 @@ - [ ] Update the changelog `## Unreleased` section to the new version `CHANGELOG.md` Make sure to leave the `## Unreleased` line unchanged for easier merges +- [ ] Ensure all GitHub API credentials from the `chatterino-ci` user are still valid + ## After the PR has been merged - [ ] Tag the release - [ ] Manually run the [create-installer](https://github.com/Chatterino/chatterino2/actions/workflows/create-installer.yml) workflow. This is only necessary if the tag was created after the CI in the main branch finished. -- [ ] Start a manual [Launchpad import](https://code.launchpad.net/~pajlada/chatterino/+git/chatterino) - scroll down & click Import Now -- [ ] Make a PPA release to [this repo](https://git.launchpad.net/~pajlada/+git/chatterino-packaging/) with the `debchange` command. - `debchange -v 2.4.0` then add the changelog entries - `debchange --release` then change the distro to be `unstable` +- [ ] If the winget releaser action doesn't work as expected, you can run this manually using [Komac](https://github.com/russellbanks/Komac), replacing `v2.5.2` with the current release: + `komac update ChatterinoTeam.Chatterino --version 2.5.2 --urls https://github.com/Chatterino/chatterino2/releases/download/v2.5.2/Chatterino.Installer.exe` + +## After the binaries have been uploaded to fourtf's bucket + +- [ ] Re-run the Publish Homebrew Cask on Release action +- [ ] Update links in the Chatterino website to point to the new release diff --git a/src/common/Channel.cpp b/src/common/Channel.cpp index 6396ef00c04..7923c706731 100644 --- a/src/common/Channel.cpp +++ b/src/common/Channel.cpp @@ -139,6 +139,18 @@ void Channel::addOrReplaceTimeout(MessagePtr message, QTime now) // WindowManager::instance().repaintVisibleChatWidgets(this); } +void Channel::addOrReplaceClearChat(MessagePtr message, QTime now) +{ + addOrReplaceChannelClear( + this->getMessageSnapshot(), std::move(message), now, + [this](auto /*idx*/, auto msg, auto replacement) { + this->replaceMessage(msg, replacement); + }, + [this](auto msg) { + this->addMessage(msg, MessageContext::Original); + }); +} + void Channel::disableAllMessages() { LimitedQueueSnapshot snapshot = this->getMessageSnapshot(); diff --git a/src/common/Channel.hpp b/src/common/Channel.hpp index c7d006f1be2..f6626f12647 100644 --- a/src/common/Channel.hpp +++ b/src/common/Channel.hpp @@ -92,6 +92,7 @@ class Channel : public std::enable_shared_from_this, public MessageSink void fillInMissingMessages(const std::vector &messages); void addOrReplaceTimeout(MessagePtr message, QTime now) final; + void addOrReplaceClearChat(MessagePtr message, QTime now) final; void disableAllMessages() final; void replaceMessage(const MessagePtr &message, const MessagePtr &replacement); diff --git a/src/controllers/plugins/api/HTTPRequest.cpp b/src/controllers/plugins/api/HTTPRequest.cpp index a1a97f616c8..a270e815147 100644 --- a/src/controllers/plugins/api/HTTPRequest.cpp +++ b/src/controllers/plugins/api/HTTPRequest.cpp @@ -49,12 +49,12 @@ void HTTPRequest::createUserType(sol::table &c2) ); } -void HTTPRequest::on_success(sol::protected_function func) +void HTTPRequest::on_success(sol::main_protected_function func) { this->cbSuccess = std::make_optional(func); } -void HTTPRequest::on_error(sol::protected_function func) +void HTTPRequest::on_error(sol::main_protected_function func) { this->cbError = std::make_optional(func); } @@ -64,7 +64,7 @@ void HTTPRequest::set_timeout(int timeout) this->timeout_ = timeout; } -void HTTPRequest::finally(sol::protected_function func) +void HTTPRequest::finally(sol::main_protected_function func) { this->cbFinally = std::make_optional(func); } diff --git a/src/controllers/plugins/api/HTTPRequest.hpp b/src/controllers/plugins/api/HTTPRequest.hpp index ebf82967f74..825894109a0 100644 --- a/src/controllers/plugins/api/HTTPRequest.hpp +++ b/src/controllers/plugins/api/HTTPRequest.hpp @@ -48,9 +48,9 @@ class HTTPRequest : public std::enable_shared_from_this int timeout_ = 10'000; bool done = false; - std::optional cbSuccess; - std::optional cbError; - std::optional cbFinally; + std::optional cbSuccess; + std::optional cbError; + std::optional cbFinally; public: // These functions are wrapped so data can be accessed more easily. When a call from Lua comes in: @@ -64,7 +64,7 @@ class HTTPRequest : public std::enable_shared_from_this * @lua@param callback c2.HTTPCallback Function to call when the HTTP request succeeds * @exposed c2.HTTPRequest:on_success */ - void on_success(sol::protected_function func); + void on_success(sol::main_protected_function func); /** * Sets the failure callback @@ -72,7 +72,7 @@ class HTTPRequest : public std::enable_shared_from_this * @lua@param callback c2.HTTPCallback Function to call when the HTTP request fails or returns a non-ok status * @exposed c2.HTTPRequest:on_error */ - void on_error(sol::protected_function func); + void on_error(sol::main_protected_function func); /** * Sets the finally callback @@ -80,7 +80,7 @@ class HTTPRequest : public std::enable_shared_from_this * @lua@param callback fun(): nil Function to call when the HTTP request finishes * @exposed c2.HTTPRequest:finally */ - void finally(sol::protected_function func); + void finally(sol::main_protected_function func); /** * Sets the timeout diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index 38d2170bc3e..8dd852ef6ff 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -53,6 +53,7 @@ #include #include #include +#include #include #include @@ -1986,6 +1987,46 @@ MessagePtr MessageBuilder::makeLowTrustUpdateMessage( return builder.release(); } +MessagePtrMut MessageBuilder::makeClearChatMessage(QTime now, + const QString &actor, + uint32_t count) +{ + MessageBuilder builder; + builder.emplace(now); + builder->count = count; + builder->parseTime = now; + builder.message().flags.set(MessageFlag::System, + MessageFlag::DoNotTriggerNotification, + MessageFlag::ClearChat); + + QString messageText; + if (actor.isEmpty()) + { + builder.emplaceSystemTextAndUpdate( + "Chat has been cleared by a moderator.", messageText); + } + else + { + builder.message().flags.set(MessageFlag::PubSub); + builder.emplace(actor, actor, MessageColor::System, + MessageColor::System); + messageText = actor + ' '; + builder.emplaceSystemTextAndUpdate("cleared the chat.", messageText); + builder->timeoutUser = actor; + } + + if (count > 1) + { + builder.emplaceSystemTextAndUpdate( + '(' % QString::number(count) % u" times)", messageText); + } + + builder->messageText = messageText; + builder->searchText = messageText; + + return builder.release(); +} + std::pair MessageBuilder::makeIrcMessage( /* mutable */ Channel *channel, const Communi::IrcMessage *ircMessage, const MessageParseArgs &args, /* mutable */ QString content, diff --git a/src/messages/MessageBuilder.hpp b/src/messages/MessageBuilder.hpp index 122348b06cf..76f0ea47bcf 100644 --- a/src/messages/MessageBuilder.hpp +++ b/src/messages/MessageBuilder.hpp @@ -258,6 +258,12 @@ class MessageBuilder const QVariantMap &tags, const QTime &time); + /// "Chat has been cleared by a moderator." or "{actor} cleared the chat." + /// @param actor The user who cleared the chat (empty if unknown) + /// @param count How many times this message has been received already + static MessagePtrMut makeClearChatMessage(QTime now, const QString &actor, + uint32_t count = 1); + private: struct TextState { TwitchChannel *twitchChannel = nullptr; diff --git a/src/messages/MessageFlag.hpp b/src/messages/MessageFlag.hpp index 306587a0982..317cf5a9754 100644 --- a/src/messages/MessageFlag.hpp +++ b/src/messages/MessageFlag.hpp @@ -54,6 +54,8 @@ enum class MessageFlag : std::int64_t { SharedMessage = (1LL << 37), /// AutoMod message that showed up due to containing a blocked term in the channel AutoModBlockedTerm = (1LL << 38), + /// The message is a full clear chat message (/clear) + ClearChat = (1LL << 39), }; using MessageFlags = FlagsEnum; diff --git a/src/messages/MessageSink.hpp b/src/messages/MessageSink.hpp index e720a1867e0..0a96f1217aa 100644 --- a/src/messages/MessageSink.hpp +++ b/src/messages/MessageSink.hpp @@ -48,6 +48,11 @@ class MessageSink virtual void addOrReplaceTimeout(MessagePtr clearchatMessage, QTime now) = 0; + /// Adds a clear chat message (for the entire chat) or merges it into an + /// existing one + virtual void addOrReplaceClearChat(MessagePtr clearchatMessage, + QTime now) = 0; + /// Flags all messages as `Disabled` virtual void disableAllMessages() = 0; diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 9864b36442f..075ef6f8795 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -197,9 +197,8 @@ std::optional parseClearChatMessage( if (message->parameters().length() == 1) { return ClearChatMessage{ - .message = - makeSystemMessage("Chat has been cleared by a moderator.", - calculateMessageTime(message).time()), + .message = MessageBuilder::makeClearChatMessage( + calculateMessageTime(message).time(), {}), .disableAllMessages = true, }; } @@ -320,15 +319,14 @@ void IrcMessageHandler::parseMessageInto(Communi::IrcMessage *message, return; } auto &clearChat = *cc; + auto time = calculateMessageTime(message).time(); if (clearChat.disableAllMessages) { - sink.addMessage(std::move(clearChat.message), - MessageContext::Original); + sink.addOrReplaceClearChat(std::move(clearChat.message), time); } else { - sink.addOrReplaceTimeout(std::move(clearChat.message), - calculateMessageTime(message).time()); + sink.addOrReplaceTimeout(std::move(clearChat.message), time); } } } @@ -464,18 +462,16 @@ void IrcMessageHandler::handleClearChatMessage(Communi::IrcMessage *message) return; } + auto time = calculateMessageTime(message).time(); // chat has been cleared by a moderator if (clearChat.disableAllMessages) { chan->disableAllMessages(); - chan->addMessage(std::move(clearChat.message), - MessageContext::Original); - + chan->addOrReplaceClearChat(std::move(clearChat.message), time); return; } - chan->addOrReplaceTimeout(std::move(clearChat.message), - calculateMessageTime(message).time()); + chan->addOrReplaceTimeout(std::move(clearChat.message), time); // refresh all getApp()->getWindows()->repaintVisibleChatWidgets(chan.get()); diff --git a/src/providers/twitch/TwitchIrcServer.cpp b/src/providers/twitch/TwitchIrcServer.cpp index 69d53a78736..6e07346b010 100644 --- a/src/providers/twitch/TwitchIrcServer.cpp +++ b/src/providers/twitch/TwitchIrcServer.cpp @@ -247,11 +247,10 @@ void TwitchIrcServer::initialize() return; } - QString text = - QString("%1 cleared the chat.").arg(action.source.login); - - postToThread([chan, text] { - chan->addSystemMessage(text); + postToThread([chan, actor{action.source.login}] { + auto now = QTime::currentTime(); + chan->addOrReplaceClearChat( + MessageBuilder::makeClearChatMessage(now, actor), now); }); }); diff --git a/src/singletons/Updates.cpp b/src/singletons/Updates.cpp index 0aad6c22078..6141a2f7223 100644 --- a/src/singletons/Updates.cpp +++ b/src/singletons/Updates.cpp @@ -125,15 +125,15 @@ void Updates::installUpdates() QMessageBox::Information, "Chatterino Update", "A link will open in your browser. Download and install to update."); box->setAttribute(Qt::WA_DeleteOnClose); - box->exec(); + box->open(); QDesktopServices::openUrl(this->updateExe_); #elif defined Q_OS_LINUX QMessageBox *box = new QMessageBox(QMessageBox::Information, "Chatterino Update", "Automatic updates are currently not available on " - "linux. Please redownload the app to update."); + "Linux. Please redownload the app to update."); box->setAttribute(Qt::WA_DeleteOnClose); - box->exec(); + box->open(); QDesktopServices::openUrl(this->updateGuideLink_); #elif defined Q_OS_WIN if (Modes::instance().isPortable) diff --git a/src/util/ChannelHelpers.hpp b/src/util/ChannelHelpers.hpp index f53f3aa2332..2dbe39b0901 100644 --- a/src/util/ChannelHelpers.hpp +++ b/src/util/ChannelHelpers.hpp @@ -118,4 +118,81 @@ void addOrReplaceChannelTimeout(const Buf &buffer, MessagePtr message, } } +/// Adds a clear message or replaces a previous one sent in the last 20 messages and in the last 5s. +/// This function accepts any buffer to store the messsages in. +/// @param replaceMessage A function of type `void (int index, MessagePtr toReplace, MessagePtr replacement)` +/// - replace `buffer[i]` (=toReplace) with `replacement` +/// @param addMessage A function of type `void (MessagePtr message)` +/// - adds the `message`. +template +void addOrReplaceChannelClear(const Buffer &buffer, MessagePtr message, + QTime now, Replace replaceMessage, Add addMessage) +{ + // NOTE: This function uses the messages PARSE time to figure out whether they should be replaced + // This works as expected for incoming messages, but not for historic messages. + // This has never worked before, but would be nice in the future. + // For this to work, we need to make sure *all* messages have a "server received time". + auto snapshotLength = static_cast(buffer.size()); + auto end = std::max(0, snapshotLength - 20); + bool shouldAddMessage = true; + QTime minimumTime = now.addSecs(-5); + auto timeoutStackStyle = static_cast( + getSettings()->timeoutStackStyle.getValue()); + + if (timeoutStackStyle == TimeoutStackStyle::DontStack) + { + addMessage(message); + return; + } + + for (auto i = snapshotLength - 1; i >= end; --i) + { + const MessagePtr &s = buffer[i]; + + if (s->parseTime < minimumTime) + { + break; + } + + bool isClearChat = s->flags.has(MessageFlag::ClearChat); + + if (timeoutStackStyle == + TimeoutStackStyle::DontStackBeyondUserMessage && + !isClearChat) + { + break; + } + + if (!isClearChat || message->flags.has(MessageFlag::PubSub) != + s->flags.has(MessageFlag::PubSub)) + { + continue; + } + + if (timeoutStackStyle == + TimeoutStackStyle::DontStackBeyondUserMessage && + s->flags.has(MessageFlag::PubSub) && + s->timeoutUser != message->timeoutUser) + { + break; + } + + uint32_t count = s->count + 1; + + auto replacement = MessageBuilder::makeClearChatMessage( + message->parseTime, message->timeoutUser, count); + replacement->flags = message->flags; + + replaceMessage(i, s, replacement); + + shouldAddMessage = false; + break; + } + + if (shouldAddMessage) + { + addMessage(message); + } +} + } // namespace chatterino diff --git a/src/util/IncognitoBrowser.cpp b/src/util/IncognitoBrowser.cpp index 3d147b6f756..51eb0c3d8ed 100644 --- a/src/util/IncognitoBrowser.cpp +++ b/src/util/IncognitoBrowser.cpp @@ -5,6 +5,7 @@ # include "util/XDGHelper.hpp" #endif +#include #include #include @@ -12,52 +13,6 @@ namespace { using namespace chatterino; -QString getPrivateSwitch(const QString &browserExecutable) -{ - // list of command line switches to turn on private browsing in browsers - static auto switches = std::vector>{ - {"firefox", "-private-window"}, - {"librewolf", "-private-window"}, - {"waterfox", "-private-window"}, - {"icecat", "-private-window"}, - {"chrome", "-incognito"}, - {"google-chrome-stable", "-incognito"}, - {"vivaldi", "-incognito"}, - {"opera", "-newprivatetab"}, - {"opera\\launcher", "--private"}, - {"iexplore", "-private"}, - {"msedge", "-inprivate"}, - {"firefox-esr", "-private-window"}, - {"chromium", "-incognito"}, - {"brave", "-incognito"}, - {"firefox-devedition", "-private-window"}, - {"firefox-developer-edition", "-private-window"}, - {"firefox-beta", "-private-window"}, - {"firefox-nightly", "-private-window"}, - }; - - // compare case-insensitively - auto lowercasedBrowserExecutable = browserExecutable.toLower(); - -#ifdef Q_OS_WINDOWS - if (lowercasedBrowserExecutable.endsWith(".exe")) - { - lowercasedBrowserExecutable.chop(4); - } -#endif - - for (const auto &switch_ : switches) - { - if (lowercasedBrowserExecutable.endsWith(switch_.first)) - { - return switch_.second; - } - } - - // couldn't match any browser -> unknown browser - return {}; -} - QString getDefaultBrowserExecutable() { #ifdef USEWINSDK @@ -102,9 +57,61 @@ QString getDefaultBrowserExecutable() } } // namespace +// + +namespace chatterino::incognitobrowser::detail { + +QString getPrivateSwitch(const QString &browserExecutable) +{ + static auto switches = std::vector>{ + {"librewolf", "-private-window"}, + {"waterfox", "-private-window"}, + {"icecat", "-private-window"}, + {"chrome", "-incognito"}, + {"google-chrome-stable", "-incognito"}, + {"vivaldi", "-incognito"}, + {"opera", "-newprivatetab"}, + {"msedge", "-inprivate"}, + {"chromium", "-incognito"}, + {"brave", "-incognito"}, + }; + + // the browser executable may be a full path, strip it to its basename and + // compare case insensitively + auto lowercasedBrowserExecutable = + QFileInfo(browserExecutable).baseName().toLower(); + +#ifdef Q_OS_WINDOWS + if (lowercasedBrowserExecutable.endsWith(".exe")) + { + lowercasedBrowserExecutable.chop(4); + } +#endif + + for (const auto &switch_ : switches) + { + if (lowercasedBrowserExecutable == switch_.first) + { + return switch_.second; + } + } + + // catch all mozilla distributed variants + if (lowercasedBrowserExecutable.startsWith("firefox")) + { + return "-private-window"; + } + + // couldn't match any browser -> unknown browser + return {}; +} + +} // namespace chatterino::incognitobrowser::detail namespace chatterino { +using namespace chatterino::incognitobrowser::detail; + bool supportsIncognitoLinks() { auto browserExe = getDefaultBrowserExecutable(); diff --git a/src/util/IncognitoBrowser.hpp b/src/util/IncognitoBrowser.hpp index 9db1e800f4b..86908aa2d8d 100644 --- a/src/util/IncognitoBrowser.hpp +++ b/src/util/IncognitoBrowser.hpp @@ -2,6 +2,12 @@ #include +namespace chatterino::incognitobrowser::detail { + +QString getPrivateSwitch(const QString &browserExecutable); + +} // namespace chatterino::incognitobrowser::detail + namespace chatterino { bool supportsIncognitoLinks(); diff --git a/src/util/VectorMessageSink.cpp b/src/util/VectorMessageSink.cpp index 3911fee890d..16aa618c994 100644 --- a/src/util/VectorMessageSink.cpp +++ b/src/util/VectorMessageSink.cpp @@ -38,6 +38,20 @@ void VectorMessageSink::addOrReplaceTimeout(MessagePtr clearchatMessage, false); } +void VectorMessageSink::addOrReplaceClearChat(MessagePtr clearchatMessage, + QTime now) +{ + addOrReplaceChannelClear( + this->messages_, std::move(clearchatMessage), now, + [&](auto idx, auto /*msg*/, auto &&replacement) { + replacement->flags.set(this->additionalFlags); + this->messages_[idx] = replacement; + }, + [&](auto &&msg) { + this->messages_.emplace_back(msg); + }); +} + void VectorMessageSink::disableAllMessages() { if (this->additionalFlags.has(MessageFlag::RecentMessage)) diff --git a/src/util/VectorMessageSink.hpp b/src/util/VectorMessageSink.hpp index c4ffcfa9c09..98ab241999a 100644 --- a/src/util/VectorMessageSink.hpp +++ b/src/util/VectorMessageSink.hpp @@ -15,6 +15,7 @@ class VectorMessageSink final : public MessageSink MessagePtr message, MessageContext ctx, std::optional overridingFlags = std::nullopt) override; void addOrReplaceTimeout(MessagePtr clearchatMessage, QTime now) override; + void addOrReplaceClearChat(MessagePtr clearchatMessage, QTime now) override; void disableAllMessages() override; diff --git a/src/widgets/Label.cpp b/src/widgets/Label.cpp index 6f56acb2767..6fe1bcdadc9 100644 --- a/src/widgets/Label.cpp +++ b/src/widgets/Label.cpp @@ -64,6 +64,18 @@ void Label::setHasOffset(bool hasOffset) this->hasOffset_ = hasOffset; this->updateSize(); } + +bool Label::getWordWrap() const +{ + return this->wordWrap_; +} + +void Label::setWordWrap(bool wrap) +{ + this->wordWrap_ = wrap; + this->update(); +} + void Label::setFontStyle(FontStyle style) { this->fontStyle_ = style; @@ -107,7 +119,14 @@ void Label::paintEvent(QPaintEvent *) painter.setBrush(this->palette().windowText()); QTextOption option(alignment); - option.setWrapMode(QTextOption::NoWrap); + if (this->wordWrap_) + { + option.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); + } + else + { + option.setWrapMode(QTextOption::NoWrap); + } painter.drawText(textRect, this->text_, option); #if 0 diff --git a/src/widgets/Label.hpp b/src/widgets/Label.hpp index 285980641bf..f3a7c845dd0 100644 --- a/src/widgets/Label.hpp +++ b/src/widgets/Label.hpp @@ -27,6 +27,9 @@ class Label : public BaseWidget bool getHasOffset() const; void setHasOffset(bool hasOffset); + bool getWordWrap() const; + void setWordWrap(bool wrap); + protected: void scaleChangedEvent(float scale_) override; void paintEvent(QPaintEvent *) override; @@ -43,6 +46,7 @@ class Label : public BaseWidget QSize preferedSize_; bool centered_ = false; bool hasOffset_ = true; + bool wordWrap_ = false; pajlada::Signals::SignalHolder connections_; }; diff --git a/src/widgets/dialogs/UpdateDialog.cpp b/src/widgets/dialogs/UpdateDialog.cpp index 64ec27eec46..a1084b2a702 100644 --- a/src/widgets/dialogs/UpdateDialog.cpp +++ b/src/widgets/dialogs/UpdateDialog.cpp @@ -19,7 +19,8 @@ UpdateDialog::UpdateDialog() LayoutCreator(this).setLayoutType(); layout.emplace