From 9de92e1b6c089fddf513a12f29012dc9e45e1297 Mon Sep 17 00:00:00 2001 From: Slava Monich Date: Wed, 11 Nov 2020 01:49:04 +0200 Subject: [PATCH 1/3] Added rlottie submodule Rendered for Lottie-Animations. --- .gitmodules | 3 ++ harbour-fernschreiber.pro | 64 +++++++++++++++++++++++++++++++++++++++ rlottie | 1 + 3 files changed, 68 insertions(+) create mode 100644 .gitmodules create mode 160000 rlottie diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..9085c34 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "rlottie"] + path = rlottie + url = https://github.com/Samsung/rlottie.git diff --git a/harbour-fernschreiber.pro b/harbour-fernschreiber.pro index 1a6994a..3915558 100644 --- a/harbour-fernschreiber.pro +++ b/harbour-fernschreiber.pro @@ -146,3 +146,67 @@ HEADERS += \ src/tdlibreceiver.h \ src/tdlibsecrets.h \ src/tdlibwrapper.h + +# https://github.com/Samsung/rlottie.git + +RLOTTIE_CONFIG = $${PWD}/rlottie/src/vector/config.h +PRE_TARGETDEPS += $${RLOTTIE_CONFIG} +QMAKE_EXTRA_TARGETS += rlottie_config + +rlottie_config.target = $${RLOTTIE_CONFIG} +rlottie_config.commands = touch $${RLOTTIE_CONFIG} # Empty config is fine + +DEFINES += LOTTIE_THREAD_SUPPORT + +INCLUDEPATH += \ + rlottie/inc \ + rlottie/src/vector \ + rlottie/src/vector/freetype + +SOURCES += \ + rlottie/src/lottie/lottieanimation.cpp \ + rlottie/src/lottie/lottieitem.cpp \ + rlottie/src/lottie/lottieitem_capi.cpp \ + rlottie/src/lottie/lottiekeypath.cpp \ + rlottie/src/lottie/lottieloader.cpp \ + rlottie/src/lottie/lottiemodel.cpp \ + rlottie/src/lottie/lottieparser.cpp + +SOURCES += \ + rlottie/src/vector/freetype/v_ft_math.cpp \ + rlottie/src/vector/freetype/v_ft_raster.cpp \ + rlottie/src/vector/freetype/v_ft_stroker.cpp \ + rlottie/src/vector/stb/stb_image.cpp \ + rlottie/src/vector/varenaalloc.cpp \ + rlottie/src/vector/vbezier.cpp \ + rlottie/src/vector/vbitmap.cpp \ + rlottie/src/vector/vbrush.cpp \ + rlottie/src/vector/vdasher.cpp \ + rlottie/src/vector/vdrawable.cpp \ + rlottie/src/vector/vdrawhelper.cpp \ + rlottie/src/vector/vdrawhelper_common.cpp \ + rlottie/src/vector/vdrawhelper_neon.cpp \ + rlottie/src/vector/vdrawhelper_sse2.cpp \ + rlottie/src/vector/vmatrix.cpp \ + rlottie/src/vector/vimageloader.cpp \ + rlottie/src/vector/vinterpolator.cpp \ + rlottie/src/vector/vpainter.cpp \ + rlottie/src/vector/vpath.cpp \ + rlottie/src/vector/vpathmesure.cpp \ + rlottie/src/vector/vraster.cpp \ + rlottie/src/vector/vrle.cpp + +NEON = $$system(g++ -dM -E -x c++ - < /dev/null | grep __ARM_NEON__) +SSE2 = $$system(g++ -dM -E -x c++ - < /dev/null | grep __SSE2__) + +!isEmpty(NEON) { + message(Using NEON render functions) + SOURCES += rlottie/src/vector/pixman/pixman-arm-neon-asm.S +} else { + !isEmpty(SSE2) { + message(Using SSE2 render functions) + SOURCES += rlottie/src/vector/vdrawhelper_sse2.cpp + } else { + message(Using default render functions) + } +} diff --git a/rlottie b/rlottie new file mode 160000 index 0000000..bf3d272 --- /dev/null +++ b/rlottie @@ -0,0 +1 @@ +Subproject commit bf3d272df3916a0c34575ac8286cb0fe672fd0d4 From 449784883e86e9f35f068b92f247313824025a7c Mon Sep 17 00:00:00 2001 From: Slava Monich Date: Mon, 9 Nov 2020 03:32:26 +0200 Subject: [PATCH 2/3] Support for animated stickers TGS are gzipped Lottie-Animations. --- harbour-fernschreiber.pro | 10 +- qml/components/StickerPreview.qml | 109 +++++------ src/harbour-fernschreiber.cpp | 3 + src/tgsplugin.cpp | 310 ++++++++++++++++++++++++++++++ src/tgsplugin.h | 33 ++++ src/tgsplugin.json | 3 + 6 files changed, 411 insertions(+), 57 deletions(-) create mode 100644 src/tgsplugin.cpp create mode 100644 src/tgsplugin.h create mode 100644 src/tgsplugin.json diff --git a/harbour-fernschreiber.pro b/harbour-fernschreiber.pro index 3915558..2961f7c 100644 --- a/harbour-fernschreiber.pro +++ b/harbour-fernschreiber.pro @@ -14,10 +14,12 @@ TARGET = harbour-fernschreiber CONFIG += sailfishapp sailfishapp_i18n -PKGCONFIG += nemonotifications-qt5 ngf-qt5 +PKGCONFIG += nemonotifications-qt5 ngf-qt5 zlib QT += core dbus sql +DEFINES += QT_STATICPLUGIN + SOURCES += src/harbour-fernschreiber.cpp \ src/appsettings.cpp \ src/chatlistmodel.cpp \ @@ -31,7 +33,8 @@ SOURCES += src/harbour-fernschreiber.cpp \ src/stickermanager.cpp \ src/tdlibfile.cpp \ src/tdlibreceiver.cpp \ - src/tdlibwrapper.cpp + src/tdlibwrapper.cpp \ + src/tgsplugin.cpp DISTFILES += qml/harbour-fernschreiber.qml \ qml/components/AudioPreview.qml \ @@ -145,7 +148,8 @@ HEADERS += \ src/tdlibfile.h \ src/tdlibreceiver.h \ src/tdlibsecrets.h \ - src/tdlibwrapper.h + src/tdlibwrapper.h \ + src/tgsplugin.h # https://github.com/Samsung/rlottie.git diff --git a/qml/components/StickerPreview.qml b/qml/components/StickerPreview.qml index 89d6ba0..8370006 100644 --- a/qml/components/StickerPreview.qml +++ b/qml/components/StickerPreview.qml @@ -18,76 +18,77 @@ */ import QtQuick 2.6 import Sailfish.Silica 1.0 +import WerkWolf.Fernschreiber 1.0 Item { - property ListItem messageListItem - property var rawMessage: messageListItem.myMessage - property var stickerData: rawMessage.content.sticker; - property int usedFileId; + readonly property var stickerData: messageListItem.myMessage.content.sticker; + readonly property bool animated: stickerData.is_animated + readonly property bool stickerVisible: staticStickerLoader.item ? staticStickerLoader.item.visible : + animatedStickerLoader.item ? animatedStickerLoader.item.visible : false - width: stickerData.width - height: stickerData.height + implicitWidth: stickerData.width + implicitHeight: stickerData.height - Component.onCompleted: { - if (stickerData) { - if (stickerData.is_animated) { - // Use thumbnail until we can decode TGS files - usedFileId = stickerData.thumbnail.photo.id; - if (stickerData.thumbnail.photo.local.is_downloading_completed) { - stickerImage.source = stickerData.thumbnail.photo.local.path; - } else { - tdLibWrapper.downloadFile(usedFileId); - } - } else { - usedFileId = stickerData.sticker.id; - if (stickerData.sticker.local.is_downloading_completed) { - stickerImage.source = stickerData.sticker.local.path; - } else { - tdLibWrapper.downloadFile(usedFileId); + TDLibFile { + id: file + tdlib: tdLibWrapper + fileInformation: stickerData.sticker + autoLoad: true + } + + Item { + width: stickerData.width + height: stickerData.height + // (centered in image mode, text-like in sticker mode) + x: appSettings.showStickersAsImages ? (parent.width - width)/2 : + messageListItem.isOwnMessage ? (parent.width - width) : 0 + anchors.verticalCenter: parent.verticalCenter + + Loader { + id: animatedStickerLoader + anchors.fill: parent + active: animated + sourceComponent: Component { + AnimatedImage { + anchors.fill: parent + source: file.path + asynchronous: true + paused: !Qt.application.active + cache: false } } } - } - Connections { - target: tdLibWrapper - onFileUpdated: { - if (stickerData) { - if (fileId === usedFileId && fileInformation.local.is_downloading_completed) { - if (stickerData.is_animated) { - stickerData.thumbnail.photo = fileInformation; - } else { - stickerData.sticker = fileInformation; - } - stickerImage.source = fileInformation.local.path; + Loader { + id: staticStickerLoader + anchors.fill: parent + active: !animated + sourceComponent: Component { + Image { + anchors.fill: parent + source: file.path + fillMode: Image.PreserveAspectFit + autoTransform: true + asynchronous: true + visible: opacity > 0 + opacity: status === Image.Ready ? 1 : 0 + Behavior on opacity { FadeAnimation {} } } } } - } - Image { - id: stickerImage - anchors.fill: parent + Loader { + anchors.fill: parent + sourceComponent: Component { + BackgroundImage {} + } - fillMode: Image.PreserveAspectFit - autoTransform: true - asynchronous: true - visible: opacity > 0 - opacity: status === Image.Ready ? 1 : 0 - Behavior on opacity { FadeAnimation {} } - } - - Loader { - anchors.fill: parent - sourceComponent: Component { - BackgroundImage {} + active: opacity > 0 + opacity: !stickerVisible && !placeHolderDelayTimer.running ? 0.15 : 0 + Behavior on opacity { FadeAnimation {} } } - - active: opacity > 0 - opacity: !stickerImage.visible && !placeHolderDelayTimer.running ? 0.15 : 0 - Behavior on opacity { FadeAnimation {} } } Timer { diff --git a/src/harbour-fernschreiber.cpp b/src/harbour-fernschreiber.cpp index 614ec47..5491f65 100644 --- a/src/harbour-fernschreiber.cpp +++ b/src/harbour-fernschreiber.cpp @@ -38,6 +38,9 @@ #include "dbusadaptor.h" #include "processlauncher.h" #include "stickermanager.h" +#include "tgsplugin.h" + +Q_IMPORT_PLUGIN(TgsIOPlugin) int main(int argc, char *argv[]) { diff --git a/src/tgsplugin.cpp b/src/tgsplugin.cpp new file mode 100644 index 0000000..3bd67ed --- /dev/null +++ b/src/tgsplugin.cpp @@ -0,0 +1,310 @@ +/* + Copyright (C) 2020 Slava Monich at al. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "tgsplugin.h" +#include "rlottie.h" + +#include +#include +#include +#include +#include +#include + +#include + +#define LOG(x) qDebug() << "[TgsIOHandler]" << qPrintable(fileName) << x +#define WARN(x) qWarning() << "[TgsIOHandler]" << qPrintable(fileName) << x + +class TgsIOHandler : public QImageIOHandler +{ +public: + static const QByteArray NAME; + static const QByteArray GZ_MAGIC; + typedef std::string ByteArray; + + TgsIOHandler(QIODevice* device, const QByteArray& format); + ~TgsIOHandler(); + + ByteArray uncompress(); + bool load(); + void render(int frameIndex); + void finishRendering(); + + // QImageIOHandler + bool canRead() const Q_DECL_OVERRIDE; + QByteArray name() const Q_DECL_OVERRIDE; + bool read(QImage* image) Q_DECL_OVERRIDE; + QVariant option(ImageOption option) const Q_DECL_OVERRIDE; + bool supportsOption(ImageOption option) const Q_DECL_OVERRIDE; + bool jumpToNextImage() Q_DECL_OVERRIDE; + bool jumpToImage(int imageNumber) Q_DECL_OVERRIDE; + int loopCount() const Q_DECL_OVERRIDE; + int imageCount() const Q_DECL_OVERRIDE; + int nextImageDelay() const Q_DECL_OVERRIDE; + int currentImageNumber() const Q_DECL_OVERRIDE; + QRect currentImageRect() const Q_DECL_OVERRIDE; + +public: + QString fileName; + QSize size; + qreal frameRate; + int frameCount; + int currentFrame; + QImage firstImage; + QImage prevImage; + QImage currentImage; + std::future currentRender; + std::unique_ptr animation; +}; + +const QByteArray TgsIOHandler::NAME("tgs"); +const QByteArray TgsIOHandler::GZ_MAGIC("\x1f\x8b"); + +TgsIOHandler::TgsIOHandler(QIODevice* device, const QByteArray& format) : + frameRate(0.), + frameCount(0), + currentFrame(0) +{ + QFileDevice* file = qobject_cast(device); + if (file) { + fileName = QFileInfo(file->fileName()).fileName(); + } + setDevice(device); + setFormat(format); +} + +TgsIOHandler::~TgsIOHandler() +{ + if (currentRender.valid()) { + currentRender.get(); + } + LOG("Done"); +} + +TgsIOHandler::ByteArray TgsIOHandler::uncompress() +{ + ByteArray unzipped; + const QByteArray zipped(device()->readAll()); + if (!zipped.isEmpty()) { + z_stream unzip; + memset(&unzip, 0, sizeof(unzip)); + unzip.next_in = (Bytef*)zipped.constData(); + // Add 16 for decoding gzip header + int zerr = inflateInit2(&unzip, MAX_WBITS + 16); + if (zerr == Z_OK) { + const uint chunk = 0x1000; + unzipped.resize(chunk); + unzip.next_out = (Bytef*)unzipped.data(); + unzip.avail_in = zipped.size(); + unzip.avail_out = chunk; + LOG("Compressed size" << zipped.size()); + while (unzip.avail_out > 0 && zerr == Z_OK) { + zerr = inflate(&unzip, Z_NO_FLUSH); + if (zerr == Z_OK && unzip.avail_out < chunk) { + // Data may get relocated, update next_out too + unzipped.resize(unzipped.size() + chunk); + unzip.next_out = (Bytef*)unzipped.data() + unzip.total_out; + unzip.avail_out += chunk; + } + } + if (zerr == Z_STREAM_END) { + unzipped.resize(unzip.next_out - (Bytef*)unzipped.data()); + LOG("Uncompressed size" << unzipped.size()); + } else { + unzipped.clear(); + } + inflateEnd(&unzip); + } + } + return unzipped; +} + +bool TgsIOHandler::load() +{ + if (!animation && device()) { + ByteArray json(uncompress()); + if (json.size() > 0) { + animation = rlottie::Animation::loadFromData(json, std::string(), std::string(), false); + if (animation) { + size_t width, height; + animation->size(width, height); + frameRate = animation->frameRate(); + frameCount = (int) animation->totalFrame(); + size = QSize(width, height); + LOG(size << frameCount << "frames," << frameRate << "fps"); + render(0); // Pre-render first frame + } + } + } + return animation != Q_NULLPTR; +} + +void TgsIOHandler::finishRendering() +{ + if (currentRender.valid()) { + currentRender.get(); + prevImage = currentImage; + if (!currentFrame && !firstImage.isNull()) { + LOG("Rendered first frame"); + firstImage = currentImage; + } + } else { + // Must be the first frame + prevImage = currentImage; + } +} + +void TgsIOHandler::render(int frameIndex) +{ + currentFrame = frameIndex % frameCount; + if (!currentFrame && !firstImage.isNull()) { + // The first frame only gets rendered once + currentImage = firstImage; + } else { + const int width = (int)size.width(); + const int height = (int)size.height(); + currentImage = QImage(width, height, QImage::Format_ARGB32_Premultiplied); + currentRender = animation->render(currentFrame, + rlottie::Surface((uint32_t*)currentImage.bits(), + width, height, currentImage.bytesPerLine())); + } +} + +bool TgsIOHandler::read(QImage* out) +{ + if (load() && frameCount > 0) { + // We must have the first frame, will wait if necessary + if (currentFrame && currentRender.valid()) { + std::future_status status = currentRender.wait_for(std::chrono::milliseconds(0)); + if (status != std::future_status::ready) { + LOG("Skipping frame" << currentFrame); + currentFrame = (currentFrame + 1) % frameCount; + *out = prevImage; + return true; + } + } + finishRendering(); + *out = currentImage; + render(currentFrame + 1); + return true; + } + return false; +} + +bool TgsIOHandler::canRead() const +{ + if (!device()) { + return false; + } else if (animation) { + return true; + } else { + // Need to support uncompressed data? + return device()->peek(2) == GZ_MAGIC; + } +} + +QByteArray TgsIOHandler::name() const +{ + return NAME; +} + +QVariant TgsIOHandler::option(ImageOption option) const +{ + switch (option) { + case Size: + ((TgsIOHandler*)this)->load(); // Cast off const + return size; + case Animation: + return true; + case ImageFormat: + return QImage::Format_ARGB32_Premultiplied; + default: + break; + } + return QVariant(); +} + +bool TgsIOHandler::supportsOption(ImageOption option) const +{ + switch(option) { + case Size: + case Animation: + case ImageFormat: + return true; + default: + break; + } + return false; +} + +bool TgsIOHandler::jumpToNextImage() +{ + if (frameCount) { + finishRendering(); + render(currentFrame + 1); + return true; + } + return false; +} + +bool TgsIOHandler::jumpToImage(int imageNumber) +{ + if (frameCount) { + if (imageNumber != currentFrame) { + finishRendering(); + render(imageNumber); + } + return true; + } + return false; +} + +int TgsIOHandler::loopCount() const +{ + return -1; +} + +int TgsIOHandler::imageCount() const +{ + return frameCount; +} + +int TgsIOHandler::currentImageNumber() const +{ + return currentFrame; +} + +QRect TgsIOHandler::currentImageRect() const +{ + return QRect(QPoint(), size); +} + +int TgsIOHandler::nextImageDelay() const +{ + return frameRate > 0 ? (int)(1000/frameRate) : 33; +} + +QImageIOPlugin::Capabilities TgsIOPlugin::capabilities(QIODevice*, const QByteArray& format) const +{ + return Capabilities((format == TgsIOHandler::NAME) ? CanRead : 0); +} + +QImageIOHandler* TgsIOPlugin::create(QIODevice* device, const QByteArray& format) const +{ + return new TgsIOHandler(device, format); +} diff --git a/src/tgsplugin.h b/src/tgsplugin.h new file mode 100644 index 0000000..dfadcf4 --- /dev/null +++ b/src/tgsplugin.h @@ -0,0 +1,33 @@ +/* + Copyright (C) 2020 Slava Monich at al. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef TGS_IMAGE_IO_PLUGIN +#define TGS_IMAGE_IO_PLUGIN + +#include +#include + +class TgsIOPlugin : public QImageIOPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "tgsplugin.json") +public: + Capabilities capabilities(QIODevice* device, const QByteArray& format) const Q_DECL_OVERRIDE; + QImageIOHandler* create(QIODevice* device, const QByteArray& format) const Q_DECL_OVERRIDE; +}; + +#endif // TGS_IMAGE_IO_PLUGIN diff --git a/src/tgsplugin.json b/src/tgsplugin.json new file mode 100644 index 0000000..ac65e14 --- /dev/null +++ b/src/tgsplugin.json @@ -0,0 +1,3 @@ +{ + "Keys": [ "tgs" ] +} From 8ca5956722ba1f77896b80d5ff0ed1acd55cdde6 Mon Sep 17 00:00:00 2001 From: Slava Monich Date: Wed, 11 Nov 2020 03:10:34 +0200 Subject: [PATCH 3/3] Made sticker animation configurable --- qml/components/StickerPreview.qml | 2 +- qml/pages/SettingsPage.qml | 9 +++++++++ src/appsettings.cpp | 15 +++++++++++++++ src/appsettings.h | 5 +++++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/qml/components/StickerPreview.qml b/qml/components/StickerPreview.qml index 8370006..68e28b0 100644 --- a/qml/components/StickerPreview.qml +++ b/qml/components/StickerPreview.qml @@ -24,7 +24,7 @@ Item { property ListItem messageListItem readonly property var stickerData: messageListItem.myMessage.content.sticker; - readonly property bool animated: stickerData.is_animated + readonly property bool animated: stickerData.is_animated && appSettings.animateStickers readonly property bool stickerVisible: staticStickerLoader.item ? staticStickerLoader.item.visible : animatedStickerLoader.item ? animatedStickerLoader.item.visible : false diff --git a/qml/pages/SettingsPage.qml b/qml/pages/SettingsPage.qml index af4b6b1..8a8962a 100644 --- a/qml/pages/SettingsPage.qml +++ b/qml/pages/SettingsPage.qml @@ -117,6 +117,15 @@ Page { text: qsTr("Appearance") } + TextSwitch { + checked: appSettings.animateStickers + text: qsTr("Animate stickers") + automaticCheck: false + onClicked: { + appSettings.animateStickers = !checked + } + } + TextSwitch { checked: appSettings.showStickersAsImages text: qsTr("Show stickers as images") diff --git a/src/appsettings.cpp b/src/appsettings.cpp index b1ccc7b..02904e6 100644 --- a/src/appsettings.cpp +++ b/src/appsettings.cpp @@ -24,6 +24,7 @@ namespace { const QString KEY_SEND_BY_ENTER("sendByEnter"); const QString KEY_USE_OPEN_WITH("useOpenWith"); const QString KEY_SHOW_STICKERS_AS_IMAGES("showStickersAsImages"); + const QString KEY_ANIMATE_STICKERS("animateStickers"); const QString KEY_NOTIFICATION_FEEDBACK("notificationFeedback"); } @@ -73,6 +74,20 @@ void AppSettings::setShowStickersAsImages(bool showAsImages) } } +bool AppSettings::animateStickers() const +{ + return settings.value(KEY_ANIMATE_STICKERS, true).toBool(); +} + +void AppSettings::setAnimateStickers(bool animate) +{ + if (animateStickers() != animate) { + LOG(KEY_ANIMATE_STICKERS << animate); + settings.setValue(KEY_ANIMATE_STICKERS, animate); + emit animateStickersChanged(); + } +} + AppSettings::NotificationFeedback AppSettings::notificationFeedback() const { return (NotificationFeedback) settings.value(KEY_NOTIFICATION_FEEDBACK, (int) NotificationFeedbackAll).toInt(); diff --git a/src/appsettings.h b/src/appsettings.h index 58b1b81..5df3d6c 100644 --- a/src/appsettings.h +++ b/src/appsettings.h @@ -26,6 +26,7 @@ class AppSettings : public QObject { Q_PROPERTY(bool sendByEnter READ getSendByEnter WRITE setSendByEnter NOTIFY sendByEnterChanged) Q_PROPERTY(bool useOpenWith READ getUseOpenWith WRITE setUseOpenWith NOTIFY useOpenWithChanged) Q_PROPERTY(bool showStickersAsImages READ showStickersAsImages WRITE setShowStickersAsImages NOTIFY showStickersAsImagesChanged) + Q_PROPERTY(bool animateStickers READ animateStickers WRITE setAnimateStickers NOTIFY animateStickersChanged) Q_PROPERTY(NotificationFeedback notificationFeedback READ notificationFeedback WRITE setNotificationFeedback NOTIFY notificationFeedbackChanged) public: @@ -48,6 +49,9 @@ public: bool showStickersAsImages() const; void setShowStickersAsImages(bool showAsImages); + bool animateStickers() const; + void setAnimateStickers(bool animate); + NotificationFeedback notificationFeedback() const; void setNotificationFeedback(NotificationFeedback feedback); @@ -55,6 +59,7 @@ signals: void sendByEnterChanged(); void useOpenWithChanged(); void showStickersAsImagesChanged(); + void animateStickersChanged(); void notificationFeedbackChanged(); private: