diff --git a/CHANGELOG.md b/CHANGELOG.md index d51288a7c..52df34528 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,8 @@ - Bugfix: Fixed a freeze from a bad regex in _Ignores_. (#4965) - Bugfix: Fixed some emotes not appearing when using _Ignores_. (#4965) - Bugfix: Fixed lookahead/-behind not working in _Ignores_. (#4965) +- Bugfix: Fixed Image Uploader accidentally deleting images with some hosts when link resolver was enabled. (#4971) +- Bugfix: Fixed rare crash with Image Uploader when closing a split right after starting an upload. (#4971) - Dev: Change clang-format from v14 to v16. (#4929) - Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791) - Dev: Temporarily disable High DPI scaling on Qt6 builds on Windows. (#4767) @@ -66,6 +68,7 @@ - Dev: `Details` file properties tab is now populated on Windows. (#4912) - Dev: Removed `Outcome` from network requests. (#4959) - Dev: Added Tests for Windows and MacOS in CI. (#4970) +- Dev: Refactored the Image Uploader feature. (#4971) ## 2.4.6 diff --git a/mocks/include/mocks/EmptyApplication.hpp b/mocks/include/mocks/EmptyApplication.hpp index 16a43db61..70ce30706 100644 --- a/mocks/include/mocks/EmptyApplication.hpp +++ b/mocks/include/mocks/EmptyApplication.hpp @@ -89,6 +89,11 @@ public: { return nullptr; } + + ImageUploader *getImageUploader() override + { + return nullptr; + } }; } // namespace chatterino::mock diff --git a/src/Application.cpp b/src/Application.cpp index bd3b5f0f6..806d21552 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -10,6 +10,7 @@ #include "controllers/hotkeys/HotkeyController.hpp" #include "controllers/ignores/IgnoreController.hpp" #include "controllers/notifications/NotificationController.hpp" +#include "singletons/ImageUploader.hpp" #ifdef CHATTERINO_HAVE_PLUGINS # include "controllers/plugins/PluginController.hpp" #endif @@ -79,6 +80,7 @@ Application::Application(Settings &_settings, Paths &_paths) , hotkeys(&this->emplace()) , windows(&this->emplace()) , toasts(&this->emplace()) + , imageUploader(&this->emplace()) , commands(&this->emplace()) , notifications(&this->emplace()) diff --git a/src/Application.hpp b/src/Application.hpp index af80308f1..3bedfd2cf 100644 --- a/src/Application.hpp +++ b/src/Application.hpp @@ -41,6 +41,7 @@ class Toasts; class ChatterinoBadges; class FfzBadges; class SeventvBadges; +class ImageUploader; class IApplication { @@ -66,6 +67,7 @@ public: virtual SeventvBadges *getSeventvBadges() = 0; virtual IUserDataController *getUserData() = 0; virtual ITwitchLiveController *getTwitchLiveController() = 0; + virtual ImageUploader *getImageUploader() = 0; }; class Application : public IApplication @@ -94,6 +96,7 @@ public: HotkeyController *const hotkeys{}; WindowManager *const windows{}; Toasts *const toasts{}; + ImageUploader *const imageUploader{}; CommandController *const commands{}; NotificationController *const notifications{}; @@ -167,6 +170,10 @@ public: } IUserDataController *getUserData() override; ITwitchLiveController *getTwitchLiveController() override; + ImageUploader *getImageUploader() override + { + return this->imageUploader; + } pajlada::Signals::NoArgSignal streamerModeChanged; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d1754239a..7a9a57565 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -424,6 +424,8 @@ set(SOURCE_FILES singletons/Emotes.hpp singletons/Fonts.cpp singletons/Fonts.hpp + singletons/ImageUploader.cpp + singletons/ImageUploader.hpp singletons/Logging.cpp singletons/Logging.hpp singletons/NativeMessaging.cpp @@ -475,8 +477,6 @@ set(SOURCE_FILES util/IpcQueue.hpp util/LayoutHelper.cpp util/LayoutHelper.hpp - util/NuulsUploader.cpp - util/NuulsUploader.hpp util/RapidjsonHelpers.cpp util/RapidjsonHelpers.hpp util/RatelimitBucket.cpp diff --git a/src/common/QLogging.cpp b/src/common/QLogging.cpp index e05ce240d..31c035d85 100644 --- a/src/common/QLogging.cpp +++ b/src/common/QLogging.cpp @@ -32,7 +32,7 @@ Q_LOGGING_CATEGORY(chatterinoNativeMessage, "chatterino.nativemessage", Q_LOGGING_CATEGORY(chatterinoNetwork, "chatterino.network", logThreshold); Q_LOGGING_CATEGORY(chatterinoNotification, "chatterino.notification", logThreshold); -Q_LOGGING_CATEGORY(chatterinoNuulsuploader, "chatterino.nuulsuploader", +Q_LOGGING_CATEGORY(chatterinoImageuploader, "chatterino.imageuploader", logThreshold); Q_LOGGING_CATEGORY(chatterinoPubSub, "chatterino.pubsub", logThreshold); Q_LOGGING_CATEGORY(chatterinoRecentMessages, "chatterino.recentmessages", diff --git a/src/common/QLogging.hpp b/src/common/QLogging.hpp index bd6ba9822..01500f1da 100644 --- a/src/common/QLogging.hpp +++ b/src/common/QLogging.hpp @@ -25,7 +25,7 @@ Q_DECLARE_LOGGING_CATEGORY(chatterinoMessage); Q_DECLARE_LOGGING_CATEGORY(chatterinoNativeMessage); Q_DECLARE_LOGGING_CATEGORY(chatterinoNetwork); Q_DECLARE_LOGGING_CATEGORY(chatterinoNotification); -Q_DECLARE_LOGGING_CATEGORY(chatterinoNuulsuploader); +Q_DECLARE_LOGGING_CATEGORY(chatterinoImageuploader); Q_DECLARE_LOGGING_CATEGORY(chatterinoPubSub); Q_DECLARE_LOGGING_CATEGORY(chatterinoRecentMessages); Q_DECLARE_LOGGING_CATEGORY(chatterinoSettings); diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index 0eb80017c..c34fe74e5 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -6,6 +6,7 @@ #include "controllers/accounts/AccountController.hpp" #include "messages/Image.hpp" #include "messages/Message.hpp" +#include "messages/MessageColor.hpp" #include "messages/MessageElement.hpp" #include "providers/LinkResolver.hpp" #include "providers/twitch/PubSubActions.hpp" @@ -660,6 +661,58 @@ MessageBuilder::MessageBuilder(LiveUpdatesUpdateEmoteSetMessageTag /*unused*/, this->message().flags.set(MessageFlag::DoNotTriggerNotification); } +MessageBuilder::MessageBuilder(ImageUploaderResultTag /*unused*/, + const QString &imageLink, + const QString &deletionLink, + size_t imagesStillQueued, size_t secondsLeft) + : MessageBuilder() +{ + this->message().flags.set(MessageFlag::System); + this->message().flags.set(MessageFlag::DoNotTriggerNotification); + + this->emplace(); + + using MEF = MessageElementFlag; + auto addText = [this](QString text, MessageElementFlags mefs = MEF::Text, + MessageColor color = + MessageColor::System) -> TextElement * { + this->message().searchText += text; + this->message().messageText += text; + return this->emplace(text, mefs, color); + }; + + addText("Your image has been uploaded to"); + + // ASSUMPTION: the user gave this uploader configuration to the program + // therefore they trust that the host is not wrong/malicious. This doesn't obey getSettings()->lowercaseDomains. + // This also ensures that the LinkResolver doesn't get these links. + addText(imageLink, {MEF::OriginalLink, MEF::LowercaseLink}, + MessageColor::Link) + ->setLink({Link::Url, imageLink}) + ->setTrailingSpace(false); + + if (!deletionLink.isEmpty()) + { + addText("(Deletion link:"); + addText(deletionLink, {MEF::OriginalLink, MEF::LowercaseLink}, + MessageColor::Link) + ->setLink({Link::Url, deletionLink}) + ->setTrailingSpace(false); + addText(")")->setTrailingSpace(false); + } + addText("."); + + if (imagesStillQueued == 0) + { + return; + } + + addText(QString("%1 left. Please wait until all of them are uploaded. " + "About %2 seconds left.") + .arg(imagesStillQueued) + .arg(secondsLeft)); +} + Message *MessageBuilder::operator->() { return this->message_.get(); diff --git a/src/messages/MessageBuilder.hpp b/src/messages/MessageBuilder.hpp index 5814b44a4..28874439b 100644 --- a/src/messages/MessageBuilder.hpp +++ b/src/messages/MessageBuilder.hpp @@ -37,6 +37,9 @@ struct LiveUpdatesAddEmoteMessageTag { }; struct LiveUpdatesUpdateEmoteSetMessageTag { }; +struct ImageUploaderResultTag { +}; + const SystemMessageTag systemMessage{}; const TimeoutMessageTag timeoutMessage{}; const LiveUpdatesUpdateEmoteMessageTag liveUpdatesUpdateEmoteMessage{}; @@ -44,6 +47,10 @@ const LiveUpdatesRemoveEmoteMessageTag liveUpdatesRemoveEmoteMessage{}; const LiveUpdatesAddEmoteMessageTag liveUpdatesAddEmoteMessage{}; const LiveUpdatesUpdateEmoteSetMessageTag liveUpdatesUpdateEmoteSetMessage{}; +// This signifies that you want to construct a message containing the result of +// a successful image upload. +const ImageUploaderResultTag imageUploaderResultMessage{}; + MessagePtr makeSystemMessage(const QString &text); MessagePtr makeSystemMessage(const QString &text, const QTime &time); std::pair makeAutomodMessage( @@ -88,6 +95,16 @@ public: MessageBuilder(LiveUpdatesUpdateEmoteSetMessageTag, const QString &platform, const QString &actor, const QString &emoteSetName); + /** + * "Your image has been uploaded to %1[ (Deletion link: %2)]." + * or "Your image has been uploaded to %1 %2. %3 left. " + * "Please wait until all of them are uploaded. " + * "About %4 seconds left." + */ + MessageBuilder(ImageUploaderResultTag, const QString &imageLink, + const QString &deletionLink, size_t imagesStillQueued = 0, + size_t secondsLeft = 0); + virtual ~MessageBuilder() = default; Message *operator->(); diff --git a/src/util/NuulsUploader.cpp b/src/singletons/ImageUploader.cpp similarity index 62% rename from src/util/NuulsUploader.cpp rename to src/singletons/ImageUploader.cpp index 1caea7d5e..d08067e26 100644 --- a/src/util/NuulsUploader.cpp +++ b/src/singletons/ImageUploader.cpp @@ -1,9 +1,10 @@ -#include "NuulsUploader.hpp" +#include "singletons/ImageUploader.hpp" #include "common/Env.hpp" #include "common/NetworkRequest.hpp" #include "common/NetworkResult.hpp" #include "common/QLogging.hpp" +#include "messages/MessageBuilder.hpp" #include "providers/twitch/TwitchMessageBuilder.hpp" #include "singletons/Paths.hpp" #include "singletons/Settings.hpp" @@ -16,6 +17,7 @@ #include #include #include +#include #include #define UPLOAD_DELAY 2000 @@ -41,13 +43,10 @@ std::optional convertToPng(const QImage &image) namespace chatterino { -// These variables are only used from the main thread. -static auto uploadMutex = QMutex(); -static std::queue uploadQueue; - // logging information on successful uploads to a json file -void logToFile(const QString originalFilePath, QString imageLink, - QString deletionLink, ChannelPtr channel) +void ImageUploader::logToFile(const QString &originalFilePath, + const QString &imageLink, + const QString &deletionLink, ChannelPtr channel) { const QString logFileName = combinePath((getSettings()->logPath.getValue().isEmpty() @@ -120,8 +119,13 @@ QString getLinkFromResponse(NetworkResult response, QString pattern) return pattern; } -void uploadImageToNuuls(RawImageData imageData, ChannelPtr channel, - ResizingTextEdit &textEdit) +void ImageUploader::save() +{ +} + +void ImageUploader::sendImageUploadRequest(RawImageData imageData, + ChannelPtr channel, + QPointer textEdit) { const static char *const boundary = "thisistheboudaryasd"; const static QString contentType = @@ -155,91 +159,103 @@ void uploadImageToNuuls(RawImageData imageData, ChannelPtr channel, .header("Content-Type", contentType) .headerList(extraHeaders) .multiPart(payload) - .onSuccess([&textEdit, channel, - originalFilePath](NetworkResult result) { - QString link = getSettings()->imageUploaderLink.getValue().isEmpty() - ? result.getData() - : getLinkFromResponse( - result, getSettings()->imageUploaderLink); - QString deletionLink = - getSettings()->imageUploaderDeletionLink.getValue().isEmpty() - ? "" - : getLinkFromResponse( - result, getSettings()->imageUploaderDeletionLink); - qCDebug(chatterinoNuulsuploader) << link << deletionLink; - textEdit.insertPlainText(link + " "); - if (uploadQueue.empty()) - { - channel->addMessage(makeSystemMessage( - QString("Your image has been uploaded to %1 %2.") - .arg(link) - .arg(deletionLink.isEmpty() - ? "" - : QString("(Deletion link: %1 )") - .arg(deletionLink)))); - uploadMutex.unlock(); - } - else - { - channel->addMessage(makeSystemMessage( - QString("Your image has been uploaded to %1 %2. %3 left. " - "Please wait until all of them are uploaded. " - "About %4 seconds left.") - .arg(link) - .arg(deletionLink.isEmpty() - ? "" - : QString("(Deletion link: %1 )") - .arg(deletionLink)) - .arg(uploadQueue.size()) - .arg(uploadQueue.size() * (UPLOAD_DELAY / 1000 + 1)))); - // 2 seconds for the timer that's there not to spam the remote server - // and 1 second of actual uploading. - - QTimer::singleShot(UPLOAD_DELAY, [channel, &textEdit]() { - uploadImageToNuuls(uploadQueue.front(), channel, textEdit); - uploadQueue.pop(); - }); - } - - logToFile(originalFilePath, link, deletionLink, channel); - }) - .onError([channel](NetworkResult result) -> bool { - auto errorMessage = - QString("An error happened while uploading your image: %1") - .arg(result.formatError()); - - // Try to read more information from the result body - auto obj = result.parseJson(); - if (!obj.isEmpty()) - { - auto apiCode = obj.value("code"); - if (!apiCode.isUndefined()) - { - auto codeString = apiCode.toVariant().toString(); - codeString.truncate(20); - errorMessage += QString(" - code: %1").arg(codeString); - } - - auto apiError = obj.value("error").toString(); - if (!apiError.isEmpty()) - { - apiError.truncate(300); - errorMessage += - QString(" - error: %1").arg(apiError.trimmed()); - } - } - - channel->addMessage(makeSystemMessage(errorMessage)); - uploadMutex.unlock(); + .onSuccess( + [textEdit, channel, originalFilePath, this](NetworkResult result) { + this->handleSuccessfulUpload(result, originalFilePath, channel, + textEdit); + }) + .onError([channel, this](NetworkResult result) -> bool { + this->handleFailedUpload(result, channel); return true; }) .execute(); } -void upload(const QMimeData *source, ChannelPtr channel, - ResizingTextEdit &outputTextEdit) +void ImageUploader::handleFailedUpload(const NetworkResult &result, + ChannelPtr channel) { - if (!uploadMutex.tryLock()) + auto errorMessage = + QString("An error happened while uploading your image: %1") + .arg(result.formatError()); + + // Try to read more information from the result body + auto obj = result.parseJson(); + if (!obj.isEmpty()) + { + auto apiCode = obj.value("code"); + if (!apiCode.isUndefined()) + { + auto codeString = apiCode.toVariant().toString(); + codeString.truncate(20); + errorMessage += QString(" - code: %1").arg(codeString); + } + + auto apiError = obj.value("error").toString(); + if (!apiError.isEmpty()) + { + apiError.truncate(300); + errorMessage += QString(" - error: %1").arg(apiError.trimmed()); + } + } + + channel->addMessage(makeSystemMessage(errorMessage)); + this->uploadMutex_.unlock(); +} + +void ImageUploader::handleSuccessfulUpload(const NetworkResult &result, + QString originalFilePath, + ChannelPtr channel, + QPointer textEdit) +{ + if (textEdit == nullptr) + { + // Split was destroyed abort further uploads + + while (!this->uploadQueue_.empty()) + { + this->uploadQueue_.pop(); + } + this->uploadMutex_.unlock(); + return; + } + QString link = + getSettings()->imageUploaderLink.getValue().isEmpty() + ? result.getData() + : getLinkFromResponse(result, getSettings()->imageUploaderLink); + QString deletionLink = + getSettings()->imageUploaderDeletionLink.getValue().isEmpty() + ? "" + : getLinkFromResponse(result, + getSettings()->imageUploaderDeletionLink); + qCDebug(chatterinoImageuploader) << link << deletionLink; + textEdit->insertPlainText(link + " "); + + // 2 seconds for the timer that's there not to spam the remote server + // and 1 second of actual uploading. + auto timeToUpload = this->uploadQueue_.size() * (UPLOAD_DELAY / 1000 + 1); + MessageBuilder builder(imageUploaderResultMessage, link, deletionLink, + this->uploadQueue_.size(), timeToUpload); + channel->addMessage(builder.release()); + if (this->uploadQueue_.empty()) + { + this->uploadMutex_.unlock(); + } + else + { + QTimer::singleShot(UPLOAD_DELAY, [channel, &textEdit, this]() { + this->sendImageUploadRequest(this->uploadQueue_.front(), channel, + textEdit); + this->uploadQueue_.pop(); + }); + } + + this->logToFile(originalFilePath, link, deletionLink, channel); +} + +void ImageUploader::upload(const QMimeData *source, ChannelPtr channel, + QPointer outputTextEdit) +{ + if (!this->uploadMutex_.tryLock()) { channel->addMessage(makeSystemMessage( QString("Please wait until the upload finishes."))); @@ -265,7 +281,7 @@ void upload(const QMimeData *source, ChannelPtr channel, { channel->addMessage( makeSystemMessage(QString("Couldn't load image :("))); - uploadMutex.unlock(); + this->uploadMutex_.unlock(); return; } @@ -273,7 +289,7 @@ void upload(const QMimeData *source, ChannelPtr channel, if (imageData) { RawImageData data = {*imageData, "png", localPath}; - uploadQueue.push(data); + this->uploadQueue_.push(data); } else { @@ -281,7 +297,7 @@ void upload(const QMimeData *source, ChannelPtr channel, QString("Cannot upload file: %1. Couldn't convert " "image to png.") .arg(localPath))); - uploadMutex.unlock(); + this->uploadMutex_.unlock(); return; } } @@ -295,11 +311,11 @@ void upload(const QMimeData *source, ChannelPtr channel, { channel->addMessage( makeSystemMessage(QString("Failed to open file. :("))); - uploadMutex.unlock(); + this->uploadMutex_.unlock(); return; } RawImageData data = {file.readAll(), "gif", localPath}; - uploadQueue.push(data); + this->uploadQueue_.push(data); file.close(); // file.readAll() => might be a bit big but it /should/ work } @@ -308,31 +324,32 @@ void upload(const QMimeData *source, ChannelPtr channel, channel->addMessage(makeSystemMessage( QString("Cannot upload file: %1. Not an image.") .arg(localPath))); - uploadMutex.unlock(); + this->uploadMutex_.unlock(); return; } } - if (!uploadQueue.empty()) + if (!this->uploadQueue_.empty()) { - uploadImageToNuuls(uploadQueue.front(), channel, outputTextEdit); - uploadQueue.pop(); + this->sendImageUploadRequest(this->uploadQueue_.front(), channel, + outputTextEdit); + this->uploadQueue_.pop(); } } else if (source->hasFormat("image/png")) { // the path to file is not present every time, thus the filePath is empty - uploadImageToNuuls({source->data("image/png"), "png", ""}, channel, - outputTextEdit); + this->sendImageUploadRequest({source->data("image/png"), "png", ""}, + channel, outputTextEdit); } else if (source->hasFormat("image/jpeg")) { - uploadImageToNuuls({source->data("image/jpeg"), "jpeg", ""}, channel, - outputTextEdit); + this->sendImageUploadRequest({source->data("image/jpeg"), "jpeg", ""}, + channel, outputTextEdit); } else if (source->hasFormat("image/gif")) { - uploadImageToNuuls({source->data("image/gif"), "gif", ""}, channel, - outputTextEdit); + this->sendImageUploadRequest({source->data("image/gif"), "gif", ""}, + channel, outputTextEdit); } else @@ -341,14 +358,14 @@ void upload(const QMimeData *source, ChannelPtr channel, auto imageData = convertToPng(image); if (imageData) { - uploadImageToNuuls({*imageData, "png", ""}, channel, - outputTextEdit); + sendImageUploadRequest({*imageData, "png", ""}, channel, + outputTextEdit); } else { channel->addMessage(makeSystemMessage( QString("Cannot upload file, failed to convert to png."))); - uploadMutex.unlock(); + this->uploadMutex_.unlock(); } } } diff --git a/src/singletons/ImageUploader.hpp b/src/singletons/ImageUploader.hpp new file mode 100644 index 000000000..260180583 --- /dev/null +++ b/src/singletons/ImageUploader.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include "common/Singleton.hpp" + +#include +#include +#include + +#include +#include + +namespace chatterino { + +class ResizingTextEdit; +class Channel; +class NetworkResult; +using ChannelPtr = std::shared_ptr; + +struct RawImageData { + QByteArray data; + QString format; + QString filePath; +}; + +class ImageUploader final : public Singleton +{ +public: + void save() override; + void upload(const QMimeData *source, ChannelPtr channel, + QPointer outputTextEdit); + +private: + void sendImageUploadRequest(RawImageData imageData, ChannelPtr channel, + QPointer textEdit); + + // This is called from the onSuccess handler of the NetworkRequest in sendImageUploadRequest + void handleSuccessfulUpload(const NetworkResult &result, + QString originalFilePath, ChannelPtr channel, + QPointer textEdit); + void handleFailedUpload(const NetworkResult &result, ChannelPtr channel); + + void logToFile(const QString &originalFilePath, const QString &imageLink, + const QString &deletionLink, ChannelPtr channel); + + // These variables are only used from the main thread. + QMutex uploadMutex_; + std::queue uploadQueue_; +}; +} // namespace chatterino diff --git a/src/util/NuulsUploader.hpp b/src/util/NuulsUploader.hpp deleted file mode 100644 index e830b4505..000000000 --- a/src/util/NuulsUploader.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include -#include - -#include - -namespace chatterino { - -class ResizingTextEdit; -class Channel; -using ChannelPtr = std::shared_ptr; - -struct RawImageData { - QByteArray data; - QString format; - QString filePath; -}; - -void upload(QByteArray imageData, ChannelPtr channel, - ResizingTextEdit &textEdit, std::string format); -void upload(RawImageData imageData, ChannelPtr channel, - ResizingTextEdit &textEdit); -void upload(const QMimeData *source, ChannelPtr channel, - ResizingTextEdit &outputTextEdit); - -} // namespace chatterino diff --git a/src/widgets/splits/Split.cpp b/src/widgets/splits/Split.cpp index 527d559c8..7565b61a2 100644 --- a/src/widgets/splits/Split.cpp +++ b/src/widgets/splits/Split.cpp @@ -16,12 +16,12 @@ #include "providers/twitch/TwitchIrcServer.hpp" #include "providers/twitch/TwitchMessageBuilder.hpp" #include "singletons/Fonts.hpp" +#include "singletons/ImageUploader.hpp" #include "singletons/Settings.hpp" #include "singletons/Theme.hpp" #include "singletons/WindowManager.hpp" #include "util/Clipboard.hpp" #include "util/Helpers.hpp" -#include "util/NuulsUploader.hpp" #include "util/StreamLink.hpp" #include "widgets/dialogs/QualityPopup.hpp" #include "widgets/dialogs/SelectChannelDialog.hpp" @@ -405,7 +405,8 @@ Split::Split(QWidget *parent) getSettings()->askOnImageUpload.setValue(false); } } - upload(source, this->getChannel(), *this->input_->ui_.textEdit); + QPointer edit = this->input_->ui_.textEdit; + getApp()->imageUploader->upload(source, this->getChannel(), edit); }); getSettings()->imageUploaderEnabled.connect(