Support displaying of photo spoilers.

This commit is contained in:
John Preston 2022-12-12 22:57:17 +04:00
parent ae819eb1a6
commit 25746d195c
26 changed files with 315 additions and 109 deletions

View file

@ -667,6 +667,8 @@ PRIVATE
history/view/media/history_view_media_common.h history/view/media/history_view_media_common.h
history/view/media/history_view_media_grouped.cpp history/view/media/history_view_media_grouped.cpp
history/view/media/history_view_media_grouped.h history/view/media/history_view_media_grouped.h
history/view/media/history_view_media_spoiler.cpp
history/view/media/history_view_media_spoiler.h
history/view/media/history_view_media_unwrapped.cpp history/view/media/history_view_media_unwrapped.cpp
history/view/media/history_view_media_unwrapped.h history/view/media/history_view_media_unwrapped.h
history/view/media/history_view_photo.cpp history/view/media/history_view_photo.cpp

View file

@ -530,9 +530,11 @@ ItemPreview Media::toGroupPreview(
MediaPhoto::MediaPhoto( MediaPhoto::MediaPhoto(
not_null<HistoryItem*> parent, not_null<HistoryItem*> parent,
not_null<PhotoData*> photo) not_null<PhotoData*> photo,
bool spoiler)
: Media(parent) : Media(parent)
, _photo(photo) { , _photo(photo)
, _spoiler(spoiler) {
parent->history()->owner().registerPhotoItem(_photo, parent); parent->history()->owner().registerPhotoItem(_photo, parent);
} }
@ -556,7 +558,7 @@ MediaPhoto::~MediaPhoto() {
std::unique_ptr<Media> MediaPhoto::clone(not_null<HistoryItem*> parent) { std::unique_ptr<Media> MediaPhoto::clone(not_null<HistoryItem*> parent) {
return _chat return _chat
? std::make_unique<MediaPhoto>(parent, _chat, _photo) ? std::make_unique<MediaPhoto>(parent, _chat, _photo)
: std::make_unique<MediaPhoto>(parent, _photo); : std::make_unique<MediaPhoto>(parent, _photo, _spoiler);
} }
PhotoData *MediaPhoto::photo() const { PhotoData *MediaPhoto::photo() const {
@ -722,17 +724,20 @@ std::unique_ptr<HistoryView::Media> MediaPhoto::createView(
return std::make_unique<HistoryView::Photo>( return std::make_unique<HistoryView::Photo>(
message, message,
realParent, realParent,
_photo); _photo,
_spoiler);
} }
MediaFile::MediaFile( MediaFile::MediaFile(
not_null<HistoryItem*> parent, not_null<HistoryItem*> parent,
not_null<DocumentData*> document, not_null<DocumentData*> document,
bool skipPremiumEffect) bool skipPremiumEffect,
bool spoiler)
: Media(parent) : Media(parent)
, _document(document) , _document(document)
, _emoji(document->sticker() ? document->sticker()->alt : QString()) , _emoji(document->sticker() ? document->sticker()->alt : QString())
, _skipPremiumEffect(skipPremiumEffect) { , _skipPremiumEffect(skipPremiumEffect)
, _spoiler(spoiler) {
parent->history()->owner().registerDocumentItem(_document, parent); parent->history()->owner().registerDocumentItem(_document, parent);
if (!_emoji.isEmpty()) { if (!_emoji.isEmpty()) {
@ -755,7 +760,8 @@ std::unique_ptr<Media> MediaFile::clone(not_null<HistoryItem*> parent) {
return std::make_unique<MediaFile>( return std::make_unique<MediaFile>(
parent, parent,
_document, _document,
!_document->session().premium()); !_document->session().premium(),
_spoiler);
} }
DocumentData *MediaFile::document() const { DocumentData *MediaFile::document() const {
@ -1096,13 +1102,15 @@ std::unique_ptr<HistoryView::Media> MediaFile::createView(
return std::make_unique<HistoryView::Gif>( return std::make_unique<HistoryView::Gif>(
message, message,
realParent, realParent,
_document); _document,
_spoiler);
} }
} else if (_document->isAnimation() || _document->isVideoFile()) { } else if (_document->isAnimation() || _document->isVideoFile()) {
return std::make_unique<HistoryView::Gif>( return std::make_unique<HistoryView::Gif>(
message, message,
realParent, realParent,
_document); _document,
_spoiler);
} else if (_document->isTheme() && _document->hasThumbnail()) { } else if (_document->isTheme() && _document->hasThumbnail()) {
return std::make_unique<HistoryView::ThemeDocument>( return std::make_unique<HistoryView::ThemeDocument>(
message, message,

View file

@ -165,7 +165,8 @@ class MediaPhoto final : public Media {
public: public:
MediaPhoto( MediaPhoto(
not_null<HistoryItem*> parent, not_null<HistoryItem*> parent,
not_null<PhotoData*> photo); not_null<PhotoData*> photo,
bool spoiler);
MediaPhoto( MediaPhoto(
not_null<HistoryItem*> parent, not_null<HistoryItem*> parent,
not_null<PeerData*> chat, not_null<PeerData*> chat,
@ -200,6 +201,7 @@ public:
private: private:
not_null<PhotoData*> _photo; not_null<PhotoData*> _photo;
PeerData *_chat = nullptr; PeerData *_chat = nullptr;
bool _spoiler = false;
}; };
@ -208,7 +210,8 @@ public:
MediaFile( MediaFile(
not_null<HistoryItem*> parent, not_null<HistoryItem*> parent,
not_null<DocumentData*> document, not_null<DocumentData*> document,
bool skipPremiumEffect); bool skipPremiumEffect,
bool spoiler);
~MediaFile(); ~MediaFile();
std::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override; std::unique_ptr<Media> clone(not_null<HistoryItem*> parent) override;
@ -242,6 +245,7 @@ private:
not_null<DocumentData*> _document; not_null<DocumentData*> _document;
QString _emoji; QString _emoji;
bool _skipPremiumEffect = false; bool _skipPremiumEffect = false;
bool _spoiler = false;
}; };

View file

@ -184,7 +184,8 @@ std::unique_ptr<Data::Media> HistoryItem::CreateMedia(
return photo->match([&](const MTPDphoto &photo) -> Result { return photo->match([&](const MTPDphoto &photo) -> Result {
return std::make_unique<Data::MediaPhoto>( return std::make_unique<Data::MediaPhoto>(
item, item,
item->history()->owner().processPhoto(photo)); item->history()->owner().processPhoto(photo),
media.is_spoiler());
}, [](const MTPDphotoEmpty &) -> Result { }, [](const MTPDphotoEmpty &) -> Result {
return nullptr; return nullptr;
}); });
@ -205,7 +206,8 @@ std::unique_ptr<Data::Media> HistoryItem::CreateMedia(
return std::make_unique<Data::MediaFile>( return std::make_unique<Data::MediaFile>(
item, item,
item->history()->owner().processDocument(document), item->history()->owner().processDocument(document),
media.is_nopremium()); media.is_nopremium(),
media.is_spoiler());
}, [](const MTPDdocumentEmpty &) -> Result { }, [](const MTPDdocumentEmpty &) -> Result {
return nullptr; return nullptr;
}); });
@ -513,10 +515,12 @@ HistoryItem::HistoryItem(
std::move(markup)); std::move(markup));
const auto skipPremiumEffect = !history->session().premium(); const auto skipPremiumEffect = !history->session().premium();
const auto spoiler = false;
_media = std::make_unique<Data::MediaFile>( _media = std::make_unique<Data::MediaFile>(
this, this,
document, document,
skipPremiumEffect); skipPremiumEffect,
spoiler);
setText(caption); setText(caption);
} }
@ -545,7 +549,8 @@ HistoryItem::HistoryItem(
postAuthor, postAuthor,
std::move(markup)); std::move(markup));
_media = std::make_unique<Data::MediaPhoto>(this, photo); const auto spoiler = false;
_media = std::make_unique<Data::MediaPhoto>(this, photo, spoiler);
setText(caption); setText(caption);
} }

View file

@ -431,6 +431,9 @@ void Element::hideSpoilers() {
if (_text.hasSpoilers()) { if (_text.hasSpoilers()) {
_text.setSpoilerRevealed(false, anim::type::instant); _text.setSpoilerRevealed(false, anim::type::instant);
} }
if (_media) {
_media->hideSpoilers();
}
} }
void Element::customEmojiRepaint() { void Element::customEmojiRepaint() {

View file

@ -7,11 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "history/view/history_view_spoiler_click_handler.h" #include "history/view/history_view_spoiler_click_handler.h"
#include "core/click_handler_types.h" // ClickHandlerContext
#include "data/data_session.h" #include "data/data_session.h"
#include "history/view/history_view_element.h" #include "history/view/history_view_element.h"
#include "history/history.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "window/window_session_controller.h"
#include "base/weak_ptr.h" #include "base/weak_ptr.h"
namespace HistoryView { namespace HistoryView {
@ -22,18 +21,13 @@ void FillTextWithAnimatedSpoilers(
if (text.hasSpoilers()) { if (text.hasSpoilers()) {
text.setSpoilerLinkFilter([weak = base::make_weak(view)]( text.setSpoilerLinkFilter([weak = base::make_weak(view)](
const ClickContext &context) { const ClickContext &context) {
const auto my = context.other.value<ClickHandlerContext>();
const auto button = context.button; const auto button = context.button;
const auto view = weak.get(); const auto view = weak.get();
if (button != Qt::LeftButton || !view || !my.elementDelegate) { if (button != Qt::LeftButton || !view) {
return false; return false;
} else if (const auto d = my.elementDelegate()) {
if (const auto controller = my.sessionWindow.get()) {
controller->session().data().registerShownSpoiler(view);
}
return true;
} }
return false; view->history()->owner().registerShownSpoiler(view);
return true;
}); });
} }
} }

View file

@ -1454,6 +1454,12 @@ TextWithEntities Document::getCaption() const {
return TextWithEntities(); return TextWithEntities();
} }
void Document::hideSpoilers() {
if (const auto captioned = Get<HistoryDocumentCaptioned>()) {
captioned->caption.setSpoilerRevealed(false, anim::type::instant);
}
}
Ui::Text::String Document::createCaption() { Ui::Text::String Document::createCaption() {
return File::createCaption(_realParent); return File::createCaption(_realParent);
} }

View file

@ -54,6 +54,7 @@ public:
} }
TextWithEntities getCaption() const override; TextWithEntities getCaption() const override;
void hideSpoilers() override;
bool needsBubble() const override { bool needsBubble() const override {
return true; return true;
} }

View file

@ -54,7 +54,7 @@ ExtendedPreview::ExtendedPreview(
, _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) { , _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) {
const auto item = parent->data(); const auto item = parent->data();
_caption = createCaption(item); _caption = createCaption(item);
_link = MakeInvoiceLink(item); _spoiler.link = MakeInvoiceLink(item);
resolveButtonText(); resolveButtonText();
} }
@ -97,15 +97,15 @@ void ExtendedPreview::ensureThumbnailRead() const {
} }
bool ExtendedPreview::hasHeavyPart() const { bool ExtendedPreview::hasHeavyPart() const {
return _animation || !_inlineThumbnail.isNull(); return _spoiler.animation || !_inlineThumbnail.isNull();
} }
void ExtendedPreview::unloadHeavyPart() { void ExtendedPreview::unloadHeavyPart() {
_inlineThumbnail _inlineThumbnail
= _imageCache = _spoiler.background
= _cornerCache = _spoiler.cornerCache
= _buttonBackground = QImage(); = _buttonBackground = QImage();
_animation = nullptr; _spoiler.animation = nullptr;
_caption.unloadPersistentAnimation(); _caption.unloadPersistentAnimation();
} }
@ -217,8 +217,8 @@ void ExtendedPreview::draw(Painter &p, const PaintContext &context) const {
fillImageShadow(p, rthumb, *rounding, context); fillImageShadow(p, rthumb, *rounding, context);
} }
validateImageCache(rthumb.size(), rounding); validateImageCache(rthumb.size(), rounding);
p.drawImage(rthumb.topLeft(), _imageCache); p.drawImage(rthumb.topLeft(), _spoiler.background);
fillSpoilerMess(p, rthumb, rounding, context); fillImageSpoiler(p, &_spoiler, rthumb, context);
paintButton(p, rthumb, context); paintButton(p, rthumb, context);
if (context.selected()) { if (context.selected()) {
fillImageOverlay(p, rthumb, rounding, context); fillImageOverlay(p, rthumb, rounding, context);
@ -263,14 +263,14 @@ void ExtendedPreview::validateImageCache(
QSize outer, QSize outer,
std::optional<Ui::BubbleRounding> rounding) const { std::optional<Ui::BubbleRounding> rounding) const {
const auto ratio = style::DevicePixelRatio(); const auto ratio = style::DevicePixelRatio();
if (_imageCache.size() == (outer * ratio) if (_spoiler.background.size() == (outer * ratio)
&& _imageCacheRounding == rounding) { && _spoiler.backgroundRounding == rounding) {
return; return;
} }
_imageCache = Images::Round( _spoiler.background = Images::Round(
prepareImageCache(outer), prepareImageCache(outer),
MediaRoundingMask(rounding)); MediaRoundingMask(rounding));
_imageCacheRounding = rounding; _spoiler.backgroundRounding = rounding;
} }
QImage ExtendedPreview::prepareImageCache(QSize outer) const { QImage ExtendedPreview::prepareImageCache(QSize outer) const {
@ -278,28 +278,6 @@ QImage ExtendedPreview::prepareImageCache(QSize outer) const {
return PrepareWithBlurredBackground(outer, {}, {}, _inlineThumbnail); return PrepareWithBlurredBackground(outer, {}, {}, _inlineThumbnail);
} }
void ExtendedPreview::fillSpoilerMess(
QPainter &p,
QRect rect,
std::optional<Ui::BubbleRounding> rounding,
const PaintContext &context) const {
if (!_animation) {
_animation = std::make_unique<Ui::SpoilerAnimation>([=] {
_parent->customEmojiRepaint();
});
history()->owner().registerHeavyViewPart(_parent);
}
_parent->clearCustomEmojiRepaint();
const auto &spoiler = Ui::DefaultImageSpoiler();
const auto index = _animation->index(context.now, context.paused);
Ui::FillSpoilerRect(
p,
rect,
MediaRoundingMask(rounding),
spoiler.frame(index),
_cornerCache);
}
void ExtendedPreview::paintButton( void ExtendedPreview::paintButton(
Painter &p, Painter &p,
QRect outer, QRect outer,
@ -318,13 +296,14 @@ void ExtendedPreview::paintButton(
const auto size = QSize(width, height); const auto size = QSize(width, height);
if (_buttonBackground.size() != size * ratio if (_buttonBackground.size() != size * ratio
|| _buttonBackgroundOverlay != overlay) { || _buttonBackgroundOverlay != overlay) {
if (_imageCache.width() < width * ratio auto &background = _spoiler.background;
|| _imageCache.height() < height * ratio) { if (background.width() < width * ratio
|| background.height() < height * ratio) {
return; return;
} }
_buttonBackground = _imageCache.copy(QRect( _buttonBackground = background.copy(QRect(
(_imageCache.width() - width * ratio) / 2, (background.width() - width * ratio) / 2,
(_imageCache.height() - height * ratio) / 2, (background.height() - height * ratio) / 2,
width * ratio, width * ratio,
height * ratio)); height * ratio));
_buttonBackground.setDevicePixelRatio(ratio); _buttonBackground.setDevicePixelRatio(ratio);
@ -374,7 +353,7 @@ TextState ExtendedPreview::textState(QPoint point, StateRequest request) const {
painth -= st::mediaCaptionSkip; painth -= st::mediaCaptionSkip;
} }
if (QRect(paintx, painty, paintw, painth).contains(point)) { if (QRect(paintx, painty, paintw, painth).contains(point)) {
result.link = _link; result.link = _spoiler.link;
} }
if (_caption.isEmpty() && _parent->media() == this) { if (_caption.isEmpty() && _parent->media() == this) {
auto fullRight = paintx + paintw; auto fullRight = paintx + paintw;
@ -402,11 +381,11 @@ TextState ExtendedPreview::textState(QPoint point, StateRequest request) const {
} }
bool ExtendedPreview::toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const { bool ExtendedPreview::toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const {
return p == _link; return p == _spoiler.link;
} }
bool ExtendedPreview::dragItemByHandler(const ClickHandlerPtr &p) const { bool ExtendedPreview::dragItemByHandler(const ClickHandlerPtr &p) const {
return p == _link; return p == _spoiler.link;
} }
bool ExtendedPreview::needInfoDisplay() const { bool ExtendedPreview::needInfoDisplay() const {
@ -420,6 +399,10 @@ TextForMimeData ExtendedPreview::selectedText(TextSelection selection) const {
return _caption.toTextForMimeData(selection); return _caption.toTextForMimeData(selection);
} }
void ExtendedPreview::hideSpoilers() {
_caption.setSpoilerRevealed(false, anim::type::instant);
}
bool ExtendedPreview::needsBubble() const { bool ExtendedPreview::needsBubble() const {
if (!_caption.isEmpty()) { if (!_caption.isEmpty()) {
return true; return true;

View file

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once #pragma once
#include "history/view/media/history_view_media.h" #include "history/view/media/history_view_media.h"
#include "history/view/media/history_view_media_spoiler.h"
enum class ImageRoundRadius; enum class ImageRoundRadius;
@ -55,6 +56,7 @@ public:
TextWithEntities getCaption() const override { TextWithEntities getCaption() const override {
return _caption.toTextWithEntities(); return _caption.toTextWithEntities();
} }
void hideSpoilers() override;
bool needsBubble() const override; bool needsBubble() const override;
bool customInfoLayout() const override { bool customInfoLayout() const override {
return _caption.isEmpty(); return _caption.isEmpty();
@ -87,23 +89,13 @@ private:
QRect outer, QRect outer,
const PaintContext &context) const; const PaintContext &context) const;
void fillSpoilerMess(
QPainter &p,
QRect rect,
std::optional<Ui::BubbleRounding> rounding,
const PaintContext &context) const;
const not_null<Data::Invoice*> _invoice; const not_null<Data::Invoice*> _invoice;
ClickHandlerPtr _link;
Ui::Text::String _caption; Ui::Text::String _caption;
mutable std::unique_ptr<Ui::SpoilerAnimation> _animation; mutable MediaSpoiler _spoiler;
mutable QImage _inlineThumbnail; mutable QImage _inlineThumbnail;
mutable QImage _imageCache;
mutable QImage _cornerCache;
mutable QImage _buttonBackground; mutable QImage _buttonBackground;
mutable QColor _buttonBackgroundOverlay; mutable QColor _buttonBackgroundOverlay;
mutable Ui::Text::String _buttonText; mutable Ui::Text::String _buttonText;
mutable std::optional<Ui::BubbleRounding> _imageCacheRounding;
mutable bool _imageCacheInvalid = false; mutable bool _imageCacheInvalid = false;
}; };

View file

@ -15,7 +15,7 @@ class FileClickHandler;
namespace HistoryView { namespace HistoryView {
class File : public Media, public base::has_weak_ptr { class File : public Media {
public: public:
File( File(
not_null<Element*> parent, not_null<Element*> parent,

View file

@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_cursor_state.h" #include "history/view/history_view_cursor_state.h"
#include "history/view/history_view_transcribe_button.h" #include "history/view/history_view_transcribe_button.h"
#include "history/view/media/history_view_media_common.h" #include "history/view/media/history_view_media_common.h"
#include "history/view/media/history_view_media_spoiler.h"
#include "window/window_session_controller.h" #include "window/window_session_controller.h"
#include "core/application.h" // Application::showDocument. #include "core/application.h" // Application::showDocument.
#include "ui/chat/attach/attach_prepare.h" #include "ui/chat/attach/attach_prepare.h"
@ -38,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/grouped_layout.h" #include "ui/grouped_layout.h"
#include "ui/cached_round_corners.h" #include "ui/cached_round_corners.h"
#include "ui/effects/path_shift_gradient.h" #include "ui/effects/path_shift_gradient.h"
#include "ui/effects/spoiler_mess.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "data/data_streaming.h" #include "data/data_streaming.h"
#include "data/data_document.h" #include "data/data_document.h"
@ -80,10 +82,12 @@ Gif::Streamed::Streamed(
Gif::Gif( Gif::Gif(
not_null<Element*> parent, not_null<Element*> parent,
not_null<HistoryItem*> realParent, not_null<HistoryItem*> realParent,
not_null<DocumentData*> document) not_null<DocumentData*> document,
bool spoiler)
: File(parent, realParent) : File(parent, realParent)
, _data(document) , _data(document)
, _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) , _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right())
, _spoiler(spoiler ? std::make_unique<MediaSpoiler>() : nullptr)
, _downloadSize(Ui::FormatSizeText(_data->size)) { , _downloadSize(Ui::FormatSizeText(_data->size)) {
setDocumentLinks(_data, realParent); setDocumentLinks(_data, realParent);
setStatusSize(Ui::FileStatusSizeReady); setStatusSize(Ui::FileStatusSizeReady);
@ -476,6 +480,13 @@ void Gif::draw(Painter &p, const PaintContext &context) const {
p.drawImage(rthumb, _thumbCache); p.drawImage(rthumb, _thumbCache);
} }
if (!isRound && _spoiler) {
fillImageSpoiler(
p,
_spoiler.get(),
rthumb,
context);
}
if (context.selected()) { if (context.selected()) {
if (isRound) { if (isRound) {
Ui::FillComplexEllipse(p, st, rthumb); Ui::FillComplexEllipse(p, st, rthumb);
@ -1312,6 +1323,13 @@ bool Gif::uploading() const {
return _data->uploading(); return _data->uploading();
} }
void Gif::hideSpoilers() {
_caption.setSpoilerRevealed(false, anim::type::instant);
if (_spoiler) {
_spoiler->revealed = false;
}
}
bool Gif::needsBubble() const { bool Gif::needsBubble() const {
if (_data->isVideoMessage()) { if (_data->isVideoMessage()) {
return false; return false;
@ -1538,12 +1556,16 @@ void Gif::parentTextUpdated() {
} }
bool Gif::hasHeavyPart() const { bool Gif::hasHeavyPart() const {
return _streamed || _dataMedia; return (_spoiler && _spoiler->animation) || _streamed || _dataMedia;
} }
void Gif::unloadHeavyPart() { void Gif::unloadHeavyPart() {
stopAnimation(); stopAnimation();
_dataMedia = nullptr; _dataMedia = nullptr;
if (_spoiler) {
_spoiler->background = _spoiler->cornerCache = QImage();
_spoiler->animation = nullptr;
}
_thumbCache = QImage(); _thumbCache = QImage();
_videoThumbnailFrame = nullptr; _videoThumbnailFrame = nullptr;
_caption.unloadPersistentAnimation(); _caption.unloadPersistentAnimation();

View file

@ -44,7 +44,8 @@ public:
Gif( Gif(
not_null<Element*> parent, not_null<Element*> parent,
not_null<HistoryItem*> realParent, not_null<HistoryItem*> realParent,
not_null<DocumentData*> document); not_null<DocumentData*> document,
bool spoiler);
~Gif(); ~Gif();
void draw(Painter &p, const PaintContext &context) const override; void draw(Painter &p, const PaintContext &context) const override;
@ -98,6 +99,7 @@ public:
TextWithEntities getCaption() const override { TextWithEntities getCaption() const override {
return _caption.toTextWithEntities(); return _caption.toTextWithEntities();
} }
void hideSpoilers() override;
bool needsBubble() const override; bool needsBubble() const override;
bool unwrapped() const override; bool unwrapped() const override;
bool customInfoLayout() const override { bool customInfoLayout() const override {
@ -206,6 +208,7 @@ private:
const not_null<DocumentData*> _data; const not_null<DocumentData*> _data;
Ui::Text::String _caption; Ui::Text::String _caption;
std::unique_ptr<Streamed> _streamed; std::unique_ptr<Streamed> _streamed;
const std::unique_ptr<MediaSpoiler> _spoiler;
mutable std::unique_ptr<TranscribeButton> _transcribe; mutable std::unique_ptr<TranscribeButton> _transcribe;
mutable std::shared_ptr<Data::DocumentMedia> _dataMedia; mutable std::shared_ptr<Data::DocumentMedia> _dataMedia;
mutable std::unique_ptr<Image> _videoThumbnailFrame; mutable std::unique_ptr<Image> _videoThumbnailFrame;

View file

@ -34,10 +34,12 @@ Invoice::Invoice(
void Invoice::fillFromData(not_null<Data::Invoice*> invoice) { void Invoice::fillFromData(not_null<Data::Invoice*> invoice) {
if (invoice->photo) { if (invoice->photo) {
const auto spoiler = false;
_attach = std::make_unique<Photo>( _attach = std::make_unique<Photo>(
_parent, _parent,
_parent->data(), _parent->data(),
invoice->photo); invoice->photo,
spoiler);
} else { } else {
_attach = nullptr; _attach = nullptr;
} }

View file

@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_cursor_state.h" #include "history/view/history_view_cursor_state.h"
#include "history/view/history_view_spoiler_click_handler.h" #include "history/view/history_view_spoiler_click_handler.h"
#include "history/view/media/history_view_sticker.h" #include "history/view/media/history_view_sticker.h"
#include "history/view/media/history_view_media_spoiler.h"
#include "storage/storage_shared_media.h" #include "storage/storage_shared_media.h"
#include "data/data_document.h" #include "data/data_document.h"
#include "data/data_session.h" #include "data/data_session.h"
@ -246,6 +247,46 @@ void Media::fillImageOverlay(
Ui::FillComplexOverlayRect(p, rect, st->msgSelectOverlay(), corners); Ui::FillComplexOverlayRect(p, rect, st->msgSelectOverlay(), corners);
} }
void Media::fillImageSpoiler(
QPainter &p,
not_null<MediaSpoiler*> spoiler,
QRect rect,
const PaintContext &context) const {
if (!spoiler->animation) {
spoiler->animation = std::make_unique<Ui::SpoilerAnimation>([=] {
_parent->customEmojiRepaint();
});
history()->owner().registerHeavyViewPart(_parent);
}
_parent->clearCustomEmojiRepaint();
Ui::FillSpoilerRect(
p,
rect,
MediaRoundingMask(spoiler->backgroundRounding),
Ui::DefaultImageSpoiler().frame(
spoiler->animation->index(context.now, context.paused)),
spoiler->cornerCache);
}
void Media::createSpoilerLink(not_null<MediaSpoiler*> spoiler) {
const auto weak = base::make_weak(this);
spoiler->link = std::make_shared<LambdaClickHandler>([=](
const ClickContext &context) {
const auto button = context.button;
const auto media = weak.get();
if (button != Qt::LeftButton || !media || spoiler->revealed) {
return;
}
const auto view = media->parent();
spoiler->revealed = true;
spoiler->revealAnimation.start([=] {
media->history()->owner().requestViewRepaint(view);
}, 0., 1., st::fadeWrapDuration);
media->history()->owner().requestViewRepaint(view);
media->history()->owner().registerShownSpoiler(view);
});
}
void Media::repaint() const { void Media::repaint() const {
history()->owner().requestViewRepaint(_parent); history()->owner().requestViewRepaint(_parent);
} }

View file

@ -32,6 +32,7 @@ struct ColorReplacements;
namespace Ui { namespace Ui {
struct BubbleSelectionInterval; struct BubbleSelectionInterval;
struct ChatPaintContext; struct ChatPaintContext;
class SpoilerAnimation;
} // namespace Ui } // namespace Ui
namespace Images { namespace Images {
@ -45,6 +46,7 @@ enum class CursorState : char;
enum class InfoDisplayType : char; enum class InfoDisplayType : char;
struct TextState; struct TextState;
struct StateRequest; struct StateRequest;
struct MediaSpoiler;
class StickerPlayer; class StickerPlayer;
class Element; class Element;
@ -74,7 +76,7 @@ enum class MediaInBubbleState : uchar {
TimeId duration, TimeId duration,
const QString &base); const QString &base);
class Media : public Object { class Media : public Object, public base::has_weak_ptr {
public: public:
explicit Media(not_null<Element*> parent) : _parent(parent) { explicit Media(not_null<Element*> parent) : _parent(parent) {
} }
@ -209,6 +211,8 @@ public:
[[nodiscard]] virtual TextWithEntities getCaption() const { [[nodiscard]] virtual TextWithEntities getCaption() const {
return TextWithEntities(); return TextWithEntities();
} }
virtual void hideSpoilers() {
}
[[nodiscard]] virtual bool needsBubble() const = 0; [[nodiscard]] virtual bool needsBubble() const = 0;
[[nodiscard]] virtual bool unwrapped() const { [[nodiscard]] virtual bool unwrapped() const {
return false; return false;
@ -343,6 +347,12 @@ protected:
QRect rect, QRect rect,
std::optional<Ui::BubbleRounding> rounding, // nullopt if in WebPage. std::optional<Ui::BubbleRounding> rounding, // nullopt if in WebPage.
const PaintContext &context) const; const PaintContext &context) const;
void fillImageSpoiler(
QPainter &p,
not_null<MediaSpoiler*> spoiler,
QRect rect,
const PaintContext &context) const;
void createSpoilerLink(not_null<MediaSpoiler*> spoiler);
void repaint() const; void repaint() const;

View file

@ -62,6 +62,7 @@ std::unique_ptr<Media> CreateAttach(
if (!collage.empty()) { if (!collage.empty()) {
return std::make_unique<GroupedMedia>(parent, collage); return std::make_unique<GroupedMedia>(parent, collage);
} else if (document) { } else if (document) {
const auto spoiler = false;
if (document->sticker()) { if (document->sticker()) {
const auto skipPremiumEffect = true; const auto skipPremiumEffect = true;
return std::make_unique<UnwrappedMedia>( return std::make_unique<UnwrappedMedia>(
@ -71,7 +72,11 @@ std::unique_ptr<Media> CreateAttach(
document, document,
skipPremiumEffect)); skipPremiumEffect));
} else if (document->isAnimation() || document->isVideoFile()) { } else if (document->isAnimation() || document->isVideoFile()) {
return std::make_unique<Gif>(parent, parent->data(), document); return std::make_unique<Gif>(
parent,
parent->data(),
document,
spoiler);
} else if (document->isWallPaper() || document->isTheme()) { } else if (document->isWallPaper() || document->isTheme()) {
return std::make_unique<ThemeDocument>( return std::make_unique<ThemeDocument>(
parent, parent,
@ -80,10 +85,12 @@ std::unique_ptr<Media> CreateAttach(
} }
return std::make_unique<Document>(parent, parent->data(), document); return std::make_unique<Document>(parent, parent->data(), document);
} else if (photo) { } else if (photo) {
const auto spoiler = false;
return std::make_unique<Photo>( return std::make_unique<Photo>(
parent, parent,
parent->data(), parent->data(),
photo); photo,
spoiler);
} else if (const auto params = ThemeDocument::ParamsFromUrl(webpageUrl)) { } else if (const auto params = ThemeDocument::ParamsFromUrl(webpageUrl)) {
return std::make_unique<ThemeDocument>(parent, nullptr, params); return std::make_unique<ThemeDocument>(parent, nullptr, params);
} }

View file

@ -694,6 +694,13 @@ TextWithEntities GroupedMedia::getCaption() const {
return main()->getCaption(); return main()->getCaption();
} }
void GroupedMedia::hideSpoilers() {
_caption.setSpoilerRevealed(false, anim::type::instant);
for (const auto &part : _parts) {
part.content->hideSpoilers();
}
}
Storage::SharedMediaTypesMask GroupedMedia::sharedMediaTypes() const { Storage::SharedMediaTypesMask GroupedMedia::sharedMediaTypes() const {
return main()->sharedMediaTypes(); return main()->sharedMediaTypes();
} }

View file

@ -67,6 +67,7 @@ public:
bool pressed) override; bool pressed) override;
TextWithEntities getCaption() const override; TextWithEntities getCaption() const override;
void hideSpoilers() override;
Storage::SharedMediaTypesMask sharedMediaTypes() const override; Storage::SharedMediaTypesMask sharedMediaTypes() const override;
bool overrideEditedDate() const override { bool overrideEditedDate() const override {

View file

@ -0,0 +1,9 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "history/view/media/history_view_media_spoiler.h"

View file

@ -0,0 +1,25 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/chat/message_bubble.h"
#include "ui/effects/animations.h"
namespace HistoryView {
struct MediaSpoiler {
ClickHandlerPtr link;
std::unique_ptr<Ui::SpoilerAnimation> animation;
QImage cornerCache;
QImage background;
std::optional<Ui::BubbleRounding> backgroundRounding;
Ui::Animations::Simple revealAnimation;
bool revealed = false;
};
} // namespace HistoryView

View file

@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_element.h" #include "history/view/history_view_element.h"
#include "history/view/history_view_cursor_state.h" #include "history/view/history_view_cursor_state.h"
#include "history/view/media/history_view_media_common.h" #include "history/view/media/history_view_media_common.h"
#include "history/view/media/history_view_media_spoiler.h"
#include "media/streaming/media_streaming_instance.h" #include "media/streaming/media_streaming_instance.h"
#include "media/streaming/media_streaming_player.h" #include "media/streaming/media_streaming_player.h"
#include "media/streaming/media_streaming_document.h" #include "media/streaming/media_streaming_document.h"
@ -20,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_session.h" #include "main/main_session.h"
#include "main/main_session_settings.h" #include "main/main_session_settings.h"
#include "ui/image/image.h" #include "ui/image/image.h"
#include "ui/effects/spoiler_mess.h"
#include "ui/chat/chat_style.h" #include "ui/chat/chat_style.h"
#include "ui/grouped_layout.h" #include "ui/grouped_layout.h"
#include "ui/cached_round_corners.h" #include "ui/cached_round_corners.h"
@ -58,10 +60,12 @@ Photo::Streamed::Streamed(
Photo::Photo( Photo::Photo(
not_null<Element*> parent, not_null<Element*> parent,
not_null<HistoryItem*> realParent, not_null<HistoryItem*> realParent,
not_null<PhotoData*> photo) not_null<PhotoData*> photo,
bool spoiler)
: File(parent, realParent) : File(parent, realParent)
, _data(photo) , _data(photo)
, _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) { , _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right())
, _spoiler(spoiler ? std::make_unique<MediaSpoiler>() : nullptr) {
_caption = createCaption(realParent); _caption = createCaption(realParent);
create(realParent->fullId()); create(realParent->fullId());
} }
@ -110,6 +114,9 @@ void Photo::create(FullMsgId contextId, PeerData *chat) {
|| _data->hasExact(PhotoSize::Thumbnail))) { || _data->hasExact(PhotoSize::Thumbnail))) {
_data->load(PhotoSize::Small, contextId); _data->load(PhotoSize::Small, contextId);
} }
if (_spoiler) {
createSpoilerLink(_spoiler.get());
}
} }
void Photo::ensureDataMediaCreated() const { void Photo::ensureDataMediaCreated() const {
@ -132,12 +139,16 @@ void Photo::dataMediaCreated() const {
} }
bool Photo::hasHeavyPart() const { bool Photo::hasHeavyPart() const {
return _streamed || _dataMedia; return (_spoiler && _spoiler->animation) || _streamed || _dataMedia;
} }
void Photo::unloadHeavyPart() { void Photo::unloadHeavyPart() {
stopAnimation(); stopAnimation();
_dataMedia = nullptr; _dataMedia = nullptr;
if (_spoiler) {
_spoiler->background = _spoiler->cornerCache = QImage();
_spoiler->animation = nullptr;
}
_imageCache = QImage(); _imageCache = QImage();
_caption.unloadPersistentAnimation(); _caption.unloadPersistentAnimation();
} }
@ -276,8 +287,22 @@ void Photo::draw(Painter &p, const PaintContext &context) const {
Assert(rounding.has_value()); Assert(rounding.has_value());
fillImageShadow(p, rthumb, *rounding, context); fillImageShadow(p, rthumb, *rounding, context);
} }
validateImageCache(rthumb.size(), rounding); const auto revealed = _spoiler
p.drawImage(rthumb.topLeft(), _imageCache); ? _spoiler->revealAnimation.value(_spoiler->revealed ? 1. : 0.)
: 1.;
if (revealed < 1.) {
validateSpoilerImageCache(rthumb.size(), rounding);
}
if (revealed > 0.) {
validateImageCache(rthumb.size(), rounding);
p.drawImage(rthumb.topLeft(), _imageCache);
}
if (revealed < 1.) {
p.setOpacity(1. - revealed);
p.drawImage(rthumb.topLeft(), _spoiler->background);
fillImageSpoiler(p, _spoiler.get(), rthumb, context);
p.setOpacity(1.);
}
if (context.selected()) { if (context.selected()) {
fillImageOverlay(p, rthumb, rounding, context); fillImageOverlay(p, rthumb, rounding, context);
} }
@ -414,9 +439,30 @@ void Photo::validateImageCache(
_imageCacheBlurred = blurredValue; _imageCacheBlurred = blurredValue;
} }
void Photo::validateSpoilerImageCache(
QSize outer,
std::optional<Ui::BubbleRounding> rounding) const {
Expects(_spoiler != nullptr);
const auto ratio = style::DevicePixelRatio();
if (_spoiler->background.size() == (outer * ratio)
&& _spoiler->backgroundRounding == rounding) {
return;
}
_spoiler->background = Images::Round(
prepareImageCacheWithLarge(outer, nullptr),
MediaRoundingMask(rounding));
_spoiler->backgroundRounding = rounding;
}
QImage Photo::prepareImageCache(QSize outer) const { QImage Photo::prepareImageCache(QSize outer) const {
return prepareImageCacheWithLarge(
outer,
_dataMedia->image(PhotoSize::Large));
}
QImage Photo::prepareImageCacheWithLarge(QSize outer, Image *large) const {
using Size = PhotoSize; using Size = PhotoSize;
const auto large = _dataMedia->image(Size::Large);
auto blurred = (Image*)nullptr; auto blurred = (Image*)nullptr;
if (const auto embedded = _dataMedia->thumbnailInline()) { if (const auto embedded = _dataMedia->thumbnailInline()) {
blurred = embedded; blurred = embedded;
@ -536,15 +582,15 @@ TextState Photo::textState(QPoint point, StateRequest request) const {
} }
if (QRect(paintx, painty, paintw, painth).contains(point)) { if (QRect(paintx, painty, paintw, painth).contains(point)) {
ensureDataMediaCreated(); ensureDataMediaCreated();
if (_data->uploading()) { result.link = (_spoiler && !_spoiler->revealed)
result.link = _cancell; ? _spoiler->link
} else if (_dataMedia->loaded()) { : _data->uploading()
result.link = _openl; ? _cancell
} else if (_data->loading()) { : _dataMedia->loaded()
result.link = _cancell; ? _openl
} else { : _data->loading()
result.link = _savel; ? _cancell
} : _savel;
} }
if (_caption.isEmpty() && _parent->media() == this) { if (_caption.isEmpty() && _parent->media() == this) {
auto fullRight = paintx + paintw; auto fullRight = paintx + paintw;
@ -593,8 +639,6 @@ void Photo::drawGrouped(
ensureDataMediaCreated(); ensureDataMediaCreated();
_dataMedia->automaticLoad(_realParent->fullId(), _parent->data()); _dataMedia->automaticLoad(_realParent->fullId(), _parent->data());
validateGroupedCache(geometry, rounding, cacheKey, cache);
const auto st = context.st; const auto st = context.st;
const auto sti = context.imageStyle(); const auto sti = context.imageStyle();
const auto loaded = _dataMedia->loaded(); const auto loaded = _dataMedia->loaded();
@ -608,7 +652,22 @@ void Photo::drawGrouped(
} }
const auto radial = isRadialAnimation(); const auto radial = isRadialAnimation();
p.drawPixmap(geometry.topLeft(), *cache); const auto revealed = _spoiler
? _spoiler->revealAnimation.value(_spoiler->revealed ? 1. : 0.)
: 1.;
if (revealed < 1.) {
validateSpoilerImageCache(geometry.size(), rounding);
}
if (revealed > 0.) {
validateGroupedCache(geometry, rounding, cacheKey, cache);
p.drawPixmap(geometry.topLeft(), *cache);
}
if (revealed < 1.) {
p.setOpacity(1. - revealed);
p.drawImage(geometry.topLeft(), _spoiler->background);
fillImageSpoiler(p, _spoiler.get(), geometry, context);
p.setOpacity(1.);
}
const auto overlayOpacity = context.selected() const auto overlayOpacity = context.selected()
? (1. - highlightOpacity) ? (1. - highlightOpacity)
@ -688,7 +747,9 @@ TextState Photo::getStateGrouped(
return {}; return {};
} }
ensureDataMediaCreated(); ensureDataMediaCreated();
return TextState(_parent, _data->uploading() return TextState(_parent, (_spoiler && !_spoiler->revealed)
? _spoiler->link
: _data->uploading()
? _cancell ? _cancell
: _dataMedia->loaded() : _dataMedia->loaded()
? _openl ? _openl
@ -904,6 +965,13 @@ TextForMimeData Photo::selectedText(TextSelection selection) const {
return _caption.toTextForMimeData(selection); return _caption.toTextForMimeData(selection);
} }
void Photo::hideSpoilers() {
_caption.setSpoilerRevealed(false, anim::type::instant);
if (_spoiler) {
_spoiler->revealed = false;
}
}
bool Photo::needsBubble() const { bool Photo::needsBubble() const {
if (!_caption.isEmpty()) { if (!_caption.isEmpty()) {
return true; return true;

View file

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/media/history_view_file.h" #include "history/view/media/history_view_file.h"
class Image;
enum class ImageRoundRadius; enum class ImageRoundRadius;
namespace Data { namespace Data {
@ -31,7 +32,8 @@ public:
Photo( Photo(
not_null<Element*> parent, not_null<Element*> parent,
not_null<HistoryItem*> realParent, not_null<HistoryItem*> realParent,
not_null<PhotoData*> photo); not_null<PhotoData*> photo,
bool spoiler);
Photo( Photo(
not_null<Element*> parent, not_null<Element*> parent,
not_null<PeerData*> chat, not_null<PeerData*> chat,
@ -81,6 +83,7 @@ public:
TextWithEntities getCaption() const override { TextWithEntities getCaption() const override {
return _caption.toTextWithEntities(); return _caption.toTextWithEntities();
} }
void hideSpoilers() override;
bool needsBubble() const override; bool needsBubble() const override;
bool customInfoLayout() const override { bool customInfoLayout() const override {
return _caption.isEmpty(); return _caption.isEmpty();
@ -130,6 +133,12 @@ private:
std::optional<Ui::BubbleRounding> rounding) const; std::optional<Ui::BubbleRounding> rounding) const;
void validateUserpicImageCache(QSize size, bool forum) const; void validateUserpicImageCache(QSize size, bool forum) const;
[[nodiscard]] QImage prepareImageCache(QSize outer) const; [[nodiscard]] QImage prepareImageCache(QSize outer) const;
void validateSpoilerImageCache(
QSize outer,
std::optional<Ui::BubbleRounding> rounding) const;
[[nodiscard]] QImage prepareImageCacheWithLarge(
QSize outer,
Image *large) const;
bool videoAutoplayEnabled() const; bool videoAutoplayEnabled() const;
void setStreamed(std::unique_ptr<Streamed> value); void setStreamed(std::unique_ptr<Streamed> value);
@ -148,6 +157,7 @@ private:
Ui::Text::String _caption; Ui::Text::String _caption;
mutable std::shared_ptr<Data::PhotoMedia> _dataMedia; mutable std::shared_ptr<Data::PhotoMedia> _dataMedia;
mutable std::unique_ptr<Streamed> _streamed; mutable std::unique_ptr<Streamed> _streamed;
const std::unique_ptr<MediaSpoiler> _spoiler;
mutable QImage _imageCache; mutable QImage _imageCache;
mutable std::optional<Ui::BubbleRounding> _imageCacheRounding; mutable std::optional<Ui::BubbleRounding> _imageCacheRounding;
int _serviceWidth : 30 = 0; int _serviceWidth : 30 = 0;

View file

@ -21,7 +21,7 @@ namespace HistoryView {
class Message; class Message;
class Poll : public Media, public base::has_weak_ptr { class Poll final : public Media {
public: public:
Poll( Poll(
not_null<Element*> parent, not_null<Element*> parent,

View file

@ -60,16 +60,19 @@ std::vector<std::unique_ptr<Data::Media>> PrepareCollageMedia(
auto result = std::vector<std::unique_ptr<Data::Media>>(); auto result = std::vector<std::unique_ptr<Data::Media>>();
result.reserve(data.items.size()); result.reserve(data.items.size());
for (const auto &item : data.items) { for (const auto &item : data.items) {
const auto spoiler = false;
if (const auto document = std::get_if<DocumentData*>(&item)) { if (const auto document = std::get_if<DocumentData*>(&item)) {
const auto skipPremiumEffect = false; const auto skipPremiumEffect = false;
result.push_back(std::make_unique<Data::MediaFile>( result.push_back(std::make_unique<Data::MediaFile>(
parent, parent,
*document, *document,
skipPremiumEffect)); skipPremiumEffect,
spoiler));
} else if (const auto photo = std::get_if<PhotoData*>(&item)) { } else if (const auto photo = std::get_if<PhotoData*>(&item)) {
result.push_back(std::make_unique<Data::MediaPhoto>( result.push_back(std::make_unique<Data::MediaPhoto>(
parent, parent,
*photo)); *photo,
spoiler));
} else { } else {
return {}; return {};
} }

@ -1 +1 @@
Subproject commit f339dd377100beaef79af8512c67a3c16f950c3f Subproject commit 64d277891403ba3275b47d6ccab849322670cf99