Allow sending media with spoilers.

This commit is contained in:
John Preston 2022-12-13 16:11:52 +04:00
parent 3a38497c4c
commit 5bee6310c0
25 changed files with 465 additions and 111 deletions

View file

@ -2327,6 +2327,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_context_animated_reactions_many#one" = "Reactions contain emoji from **{count} pack**.";
"lng_context_animated_reactions_many#other" = "Reactions contain emoji from **{count} packs**.";
"lng_context_spoiler_effect" = "Spoiler Effect";
"lng_context_disable_spoiler" = "Disable Spoiler Effect";
"lng_downloads_section" = "Downloads";
"lng_downloads_view_in_chat" = "View in chat";
"lng_downloads_view_in_section" = "View in downloads";

View file

@ -206,7 +206,7 @@ void EditMessageWithUploadedPhoto(
EditMessageWithUploadedMedia(
item,
options,
PrepareUploadedPhoto(std::move(info)));
PrepareUploadedPhoto(item, std::move(info)));
}
mtpRequestId EditCaption(

View file

@ -75,10 +75,14 @@ MTPVector<MTPDocumentAttribute> ComposeSendingDocumentAttributes(
} // namespace
MTPInputMedia PrepareUploadedPhoto(RemoteFileInfo info) {
const auto flags = info.attachedStickers.empty()
? MTPDinputMediaUploadedPhoto::Flags(0)
: MTPDinputMediaUploadedPhoto::Flag::f_stickers;
MTPInputMedia PrepareUploadedPhoto(
not_null<HistoryItem*> item,
RemoteFileInfo info) {
using Flag = MTPDinputMediaUploadedPhoto::Flag;
const auto spoiler = item->media()
&& item->media()->hasSpoiler();
const auto flags = (spoiler ? Flag::f_spoiler : Flag())
| (info.attachedStickers.empty() ? Flag() : Flag::f_stickers);
return MTP_inputMediaUploadedPhoto(
MTP_flags(flags),
info.file,
@ -93,12 +97,13 @@ MTPInputMedia PrepareUploadedDocument(
if (!item || !item->media() || !item->media()->document()) {
return MTP_inputMediaEmpty();
}
const auto emptyFlag = MTPDinputMediaUploadedDocument::Flags(0);
using DocFlags = MTPDinputMediaUploadedDocument::Flag;
const auto flags = emptyFlag
| (info.thumb ? DocFlags::f_thumb : emptyFlag)
| (item->groupId() ? DocFlags::f_nosound_video : emptyFlag)
| (info.attachedStickers.empty() ? DocFlags::f_stickers : emptyFlag);
using Flag = MTPDinputMediaUploadedDocument::Flag;
const auto spoiler = item->media()
&& item->media()->hasSpoiler();
const auto flags = (spoiler ? Flag::f_spoiler : Flag())
| (info.thumb ? Flag::f_thumb : Flag())
| (item->groupId() ? Flag::f_nosound_video : Flag())
| (info.attachedStickers.empty() ? Flag::f_stickers : Flag());
const auto document = item->media()->document();
return MTP_inputMediaUploadedDocument(
MTP_flags(flags),

View file

@ -13,7 +13,9 @@ namespace Api {
struct RemoteFileInfo;
MTPInputMedia PrepareUploadedPhoto(RemoteFileInfo info);
MTPInputMedia PrepareUploadedPhoto(
not_null<HistoryItem*> item,
RemoteFileInfo info);
MTPInputMedia PrepareUploadedDocument(
not_null<HistoryItem*> item,

View file

@ -441,13 +441,17 @@ void SendConfirmedFile(
const auto media = MTPMessageMedia([&] {
if (file->type == SendMediaType::Photo) {
using Flag = MTPDmessageMediaPhoto::Flag;
return MTP_messageMediaPhoto(
MTP_flags(MTPDmessageMediaPhoto::Flag::f_photo),
MTP_flags(Flag::f_photo
| (file->spoiler ? Flag::f_spoiler : Flag())),
file->photo,
MTPint());
} else if (file->type == SendMediaType::File) {
using Flag = MTPDmessageMediaDocument::Flag;
return MTP_messageMediaDocument(
MTP_flags(MTPDmessageMediaDocument::Flag::f_document),
MTP_flags(Flag::f_document
| (file->spoiler ? Flag::f_spoiler : Flag())),
file->document,
MTPint());
} else if (file->type == SendMediaType::Audio) {

View file

@ -3365,7 +3365,8 @@ void ApiWrap::editMedia(
std::move(file.information),
type,
to,
caption));
caption,
file.spoiler));
}
void ApiWrap::sendFiles(
@ -3406,6 +3407,7 @@ void ApiWrap::sendFiles(
uploadWithType,
to,
caption,
file.spoiler,
album));
caption = TextWithTags();
}
@ -3425,14 +3427,17 @@ void ApiWrap::sendFile(
const SendAction &action) {
const auto to = fileLoadTaskOptions(action);
auto caption = TextWithTags();
const auto spoiler = false;
const auto information = nullptr;
_fileLoader->addTask(std::make_unique<FileLoadTask>(
&session(),
QString(),
fileContent,
nullptr,
information,
type,
to,
caption));
caption,
spoiler));
}
void ApiWrap::sendUploadedPhoto(
@ -3440,7 +3445,7 @@ void ApiWrap::sendUploadedPhoto(
Api::RemoteFileInfo info,
Api::SendOptions options) {
if (const auto item = _session->data().message(localId)) {
const auto media = Api::PrepareUploadedPhoto(std::move(info));
const auto media = Api::PrepareUploadedPhoto(item, std::move(info));
if (const auto groupId = item->groupId()) {
uploadAlbumMedia(item, groupId, media);
} else {

View file

@ -215,19 +215,35 @@ rpl::producer<int> SendFilesBox::Block::itemModifyRequest() const {
void SendFilesBox::Block::setSendWay(Ui::SendFilesWay way) {
if (!_isAlbum) {
if (_isSingleMedia) {
const auto media = static_cast<Ui::SingleMediaPreview*>(
_preview.get());
media->setSendWay(way);
}
return;
}
applyAlbumOrder();
applyChanges();
const auto album = static_cast<Ui::AlbumPreview*>(_preview.get());
album->setSendWay(way);
}
void SendFilesBox::Block::applyAlbumOrder() {
void SendFilesBox::Block::applyChanges() {
if (!_isAlbum) {
if (_isSingleMedia) {
const auto media = static_cast<Ui::SingleMediaPreview*>(
_preview.get());
(*_items)[_from].spoiler = media->hasSpoiler();
}
return;
}
const auto album = static_cast<Ui::AlbumPreview*>(_preview.get());
const auto order = album->takeOrder();
const auto spoilered = album->collectSpoileredIndices();
const auto guard = gsl::finally([&] {
for (auto i = 0, count = int(order.size()); i != count; ++i) {
(*_items)[_from + i].spoiler = spoilered.contains(i);
}
});
const auto isIdentity = [&] {
for (auto i = 0, count = int(order.size()); i != count; ++i) {
if (order[i] != i) {
@ -385,19 +401,25 @@ void SendFilesBox::setupDragArea() {
areas.photo->setDroppedCallback(droppedCallback(true));
}
void SendFilesBox::refreshAllAfterChanges(int fromItem) {
void SendFilesBox::refreshAllAfterChanges(int fromItem, Fn<void()> perform) {
auto fromBlock = 0;
for (auto count = int(_blocks.size()); fromBlock != count; ++fromBlock) {
if (_blocks[fromBlock].tillIndex() >= fromItem) {
break;
}
}
for (auto index = fromBlock; index < _blocks.size(); ++index) {
_blocks[index].applyChanges();
}
if (perform) {
perform();
}
generatePreviewFrom(fromBlock);
{
auto sendWay = _sendWay.current();
sendWay.setHasCompressedStickers(_list.hasSticker());
_sendWay = sendWay;
}
generatePreviewFrom(fromBlock);
_inner->resizeToWidth(st::boxWideWidth);
refreshControls();
captionResized();
@ -489,11 +511,7 @@ void SendFilesBox::generatePreviewFrom(int fromBlock) {
using Type = Ui::PreparedFile::Type;
const auto eraseFrom = _blocks.begin() + fromBlock;
for (auto i = eraseFrom; i != _blocks.end(); ++i) {
i->applyAlbumOrder();
}
_blocks.erase(eraseFrom, _blocks.end());
_blocks.erase(_blocks.begin() + fromBlock, _blocks.end());
const auto fromItem = _blocks.empty() ? 0 : _blocks.back().tillIndex();
Assert(fromItem <= _list.files.size());
@ -559,8 +577,9 @@ void SendFilesBox::pushBlock(int from, int till) {
closeBox();
return;
}
_list.files.erase(_list.files.begin() + index);
refreshAllAfterChanges(from);
refreshAllAfterChanges(index, [&] {
_list.files.erase(_list.files.begin() + index);
});
});
}, widget->lifetime());
@ -571,8 +590,9 @@ void SendFilesBox::pushBlock(int from, int till) {
if (list.files.empty()) {
return;
}
_list.files[index] = std::move(list.files.front());
refreshAllAfterChanges(from);
refreshAllAfterChanges(from, [&] {
_list.files[index] = std::move(list.files.front());
});
};
const auto checkResult = [=](const Ui::PreparedList &list) {
if (_sendLimit != SendLimit::One) {
@ -1076,7 +1096,7 @@ void SendFilesBox::send(
}
for (auto &block : _blocks) {
block.applyAlbumOrder();
block.applyChanges();
}
Storage::ApplyModifications(_list);

View file

@ -106,7 +106,7 @@ private:
[[nodiscard]] rpl::producer<int> itemModifyRequest() const;
void setSendWay(Ui::SendFilesWay way);
void applyAlbumOrder();
void applyChanges();
private:
base::unique_qptr<Ui::RpWidget> _preview;
@ -152,7 +152,7 @@ private:
void pushBlock(int from, int till);
void openDialogToAddFileToAlbum();
void refreshAllAfterChanges(int fromItem);
void refreshAllAfterChanges(int fromItem, Fn<void()> perform = nullptr);
void enqueueNextPrepare();
void addPreparedAsyncFile(Ui::PreparedFile &&file);

View file

@ -129,7 +129,8 @@ void DicePack::generateLocal(int index, const QString &name) {
nullptr,
SendMediaType::File,
FileLoadTo(0, {}, 0, 0, 0),
{});
{},
false);
task.process({ .generateGoodThumbnail = false });
const auto result = task.peekResult();
Assert(result != nullptr);

View file

@ -446,6 +446,10 @@ QString Media::errorTextForForward(not_null<PeerData*> peer) const {
return QString();
}
bool Media::hasSpoiler() const {
return false;
}
bool Media::consumeMessageText(const TextWithEntities &text) {
return false;
}
@ -663,6 +667,10 @@ QString MediaPhoto::errorTextForForward(not_null<PeerData*> peer) const {
).value_or(QString());
}
bool MediaPhoto::hasSpoiler() const {
return _spoiler;
}
bool MediaPhoto::updateInlineResultMedia(const MTPMessageMedia &media) {
if (media.type() != mtpc_messageMediaPhoto) {
return false;
@ -1037,6 +1045,10 @@ QString MediaFile::errorTextForForward(not_null<PeerData*> peer) const {
return QString();
}
bool MediaFile::hasSpoiler() const {
return _spoiler;
}
bool MediaFile::updateInlineResultMedia(const MTPMessageMedia &media) {
if (media.type() != mtpc_messageMediaDocument) {
return false;

View file

@ -129,6 +129,7 @@ public:
virtual bool dropForwardedInfo() const;
virtual bool forceForwardedInfo() const;
virtual QString errorTextForForward(not_null<PeerData*> peer) const;
[[nodiscard]] virtual bool hasSpoiler() const;
[[nodiscard]] virtual bool consumeMessageText(
const TextWithEntities &text);
@ -190,6 +191,7 @@ public:
bool allowsEditCaption() const override;
bool allowsEditMedia() const override;
QString errorTextForForward(not_null<PeerData*> peer) const override;
bool hasSpoiler() const override;
bool updateInlineResultMedia(const MTPMessageMedia &media) override;
bool updateSentMedia(const MTPMessageMedia &media) override;
@ -233,6 +235,7 @@ public:
bool forwardedBecomesUnread() const override;
bool dropForwardedInfo() const override;
QString errorTextForForward(not_null<PeerData*> peer) const override;
bool hasSpoiler() const override;
bool updateInlineResultMedia(const MTPMessageMedia &media) override;
bool updateSentMedia(const MTPMessageMedia &media) override;

View file

@ -834,12 +834,27 @@ void Gif::validateSpoilerImageCache(
&& _spoiler->backgroundRounding == rounding) {
return;
}
const auto normal = _dataMedia->thumbnail();
auto container = std::optional<Image>();
const auto downscale = [&](Image *image) {
if (!image || (image->width() <= 40 && image->height() <= 40)) {
return image;
}
container.emplace(image->original().scaled(
{ 40, 40 },
Qt::KeepAspectRatio,
Qt::SmoothTransformation));
return &*container;
};
const auto videothumb = _videoThumbnailFrame.get();
const auto embedded = _dataMedia->thumbnailInline();
const auto blurred = embedded ? embedded : downscale(normal);
_spoiler->background = Images::Round(
PrepareWithBlurredBackground(
outer,
::Media::Streaming::ExpandDecision(),
nullptr,
_dataMedia->thumbnailInline()),
blurred),
MediaRoundingMask(rounding));
_spoiler->backgroundRounding = rounding;
}

View file

@ -1588,6 +1588,7 @@ void FormController::uploadEncryptedFile(
file.uploadData->fileId,
FileLoadTo(PeerId(), Api::SendOptions(), MsgId(), MsgId(), MsgId()),
TextWithTags(),
false,
std::shared_ptr<SendingAlbum>(nullptr));
prepared->type = SendMediaType::Secure;
prepared->content = QByteArray::fromRawData(

View file

@ -488,12 +488,14 @@ FileLoadResult::FileLoadResult(
uint64 id,
const FileLoadTo &to,
const TextWithTags &caption,
bool spoiler,
std::shared_ptr<SendingAlbum> album)
: taskId(taskId)
, id(id)
, to(to)
, album(std::move(album))
, caption(caption) {
, caption(caption)
, spoiler(spoiler) {
}
void FileLoadResult::setFileData(const QByteArray &filedata) {
@ -530,6 +532,7 @@ FileLoadTask::FileLoadTask(
SendMediaType type,
const FileLoadTo &to,
const TextWithTags &caption,
bool spoiler,
std::shared_ptr<SendingAlbum> album)
: _id(base::RandomValue<uint64>())
, _session(session)
@ -540,7 +543,8 @@ FileLoadTask::FileLoadTask(
, _content(content)
, _information(std::move(information))
, _type(type)
, _caption(caption) {
, _caption(caption)
, _spoiler(spoiler) {
Expects(to.options.scheduled
|| !to.replaceMediaOf
|| IsServerMsgId(to.replaceMediaOf));
@ -736,6 +740,7 @@ void FileLoadTask::process(Args &&args) {
_id,
_to,
_caption,
_spoiler,
_album);
QString filename, filemime;

View file

@ -224,6 +224,7 @@ struct FileLoadResult {
uint64 id,
const FileLoadTo &to,
const TextWithTags &caption,
bool spoiler,
std::shared_ptr<SendingAlbum> album);
TaskId taskId;
@ -256,6 +257,7 @@ struct FileLoadResult {
PreparedPhotoThumbs photoThumbs;
TextWithTags caption;
bool spoiler = false;
std::vector<MTPInputDocument> attachedStickers;
@ -285,6 +287,7 @@ public:
SendMediaType type,
const FileLoadTo &to,
const TextWithTags &caption,
bool spoiler,
std::shared_ptr<SendingAlbum> album = nullptr);
FileLoadTask(
not_null<Main::Session*> session,
@ -343,6 +346,7 @@ private:
VoiceWaveform _waveform;
SendMediaType _type;
TextWithTags _caption;
bool _spoiler = false;
std::shared_ptr<FileLoadResult> _result;

View file

@ -9,12 +9,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "editor/photo_editor_common.h"
#include "ui/chat/attach/attach_controls.h"
#include "ui/chat/attach/attach_prepare.h"
#include "ui/image/image_prepare.h"
#include "ui/widgets/buttons.h"
#include "ui/effects/spoiler_mess.h"
#include "ui/widgets/popup_menu.h"
#include "ui/painter.h"
#include "lang/lang_keys.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
namespace Ui {
namespace {
@ -29,7 +33,6 @@ AbstractSingleMediaPreview::AbstractSingleMediaPreview(
: AbstractSinglePreview(parent)
, _minThumbH(st::sendBoxAlbumGroupSize.height()
+ st::sendBoxAlbumGroupSkipTop * 2)
, _photoEditorButton(base::make_unique_q<AbstractButton>(this))
, _controls(base::make_unique_q<AttachControlsWidget>(this, type)) {
}
@ -44,7 +47,25 @@ rpl::producer<> AbstractSingleMediaPreview::editRequests() const {
}
rpl::producer<> AbstractSingleMediaPreview::modifyRequests() const {
return _photoEditorButton->clicks() | rpl::to_empty;
return _photoEditorRequests.events();
}
void AbstractSingleMediaPreview::setSendWay(SendFilesWay way) {
if (_sendWay != way) {
_sendWay = way;
}
update();
}
void AbstractSingleMediaPreview::setSpoiler(bool spoiler) {
_spoiler = spoiler
? std::make_unique<SpoilerAnimation>([=] { update(); })
: nullptr;
update();
}
bool AbstractSingleMediaPreview::hasSpoiler() const {
return _spoiler != nullptr;
}
void AbstractSingleMediaPreview::preparePreview(QImage preview) {
@ -105,16 +126,17 @@ void AbstractSingleMediaPreview::preparePreview(QImage preview) {
preview = Images::Opaque(std::move(preview));
_preview = PixmapFromImage(std::move(preview));
_preview.setDevicePixelRatio(style::DevicePixelRatio());
updatePhotoEditorButton();
_previewBlurred = QPixmap();
resize(width(), std::max(_previewHeight, _minThumbH));
}
void AbstractSingleMediaPreview::updatePhotoEditorButton() {
_photoEditorButton->resize(_previewWidth, _previewHeight);
_photoEditorButton->moveToLeft(_previewLeft, _previewTop);
_photoEditorButton->setVisible(isPhoto());
bool AbstractSingleMediaPreview::isOverPreview(QPoint position) const {
return QRect(
_previewLeft,
_previewTop,
_previewWidth,
_previewHeight).contains(position);
}
void AbstractSingleMediaPreview::resizeEvent(QResizeEvent *e) {
@ -127,6 +149,15 @@ void AbstractSingleMediaPreview::resizeEvent(QResizeEvent *e) {
void AbstractSingleMediaPreview::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
auto takenSpoiler = (drawBackground() || _sendWay.sendImagesAsPhotos())
? nullptr
: base::take(_spoiler);
const auto guard = gsl::finally([&] {
if (takenSpoiler) {
_spoiler = base::take(takenSpoiler);
}
});
if (drawBackground()) {
const auto &padding = st::boxPhotoPadding;
if (_previewLeft > padding.left()) {
@ -154,10 +185,23 @@ void AbstractSingleMediaPreview::paintEvent(QPaintEvent *e) {
st::confirmBg);
}
}
if (!tryPaintAnimation(p)) {
p.drawPixmap(_previewLeft, _previewTop, _preview);
if (_spoiler && _previewBlurred.isNull()) {
_previewBlurred = BlurredPreviewFromPixmap(_preview, RectPart::None);
}
if (_animated && !isAnimatedPreviewReady()) {
if (_spoiler || !tryPaintAnimation(p)) {
const auto &pixmap = _spoiler ? _previewBlurred : _preview;
const auto position = QPoint(_previewLeft, _previewTop);
p.drawPixmap(position, pixmap);
if (_spoiler) {
FillSpoilerRect(
p,
QRect(position, pixmap.size() / pixmap.devicePixelRatio()),
DefaultImageSpoiler().frame(
_spoiler->index(crl::now(), false)));
}
}
if (_animated && !isAnimatedPreviewReady() && !_spoiler) {
const auto innerSize = st::msgFileLayout.thumbSize;
auto inner = QRect(
_previewLeft + (_previewWidth - innerSize) / 2,
@ -177,6 +221,56 @@ void AbstractSingleMediaPreview::paintEvent(QPaintEvent *e) {
}
}
void AbstractSingleMediaPreview::mousePressEvent(QMouseEvent *e) {
if (isOverPreview(e->pos())) {
_pressed = true;
}
}
void AbstractSingleMediaPreview::mouseMoveEvent(QMouseEvent *e) {
applyCursor((isPhoto() && isOverPreview(e->pos()))
? style::cur_pointer
: style::cur_default);
}
void AbstractSingleMediaPreview::mouseReleaseEvent(QMouseEvent *e) {
if (base::take(_pressed) && isOverPreview(e->pos())) {
if (e->button() == Qt::RightButton) {
showContextMenu(e->globalPos());
} else if (isPhoto()) {
_photoEditorRequests.fire({});
}
}
}
void AbstractSingleMediaPreview::applyCursor(style::cursor cursor) {
if (_cursor != cursor) {
_cursor = cursor;
setCursor(_cursor);
}
}
void AbstractSingleMediaPreview::showContextMenu(QPoint position) {
if (!_sendWay.sendImagesAsPhotos()) {
return;
}
_menu = base::make_unique_q<Ui::PopupMenu>(
this,
st::popupMenuWithIcons);
_menu->addAction(hasSpoiler()
? tr::lng_context_disable_spoiler(tr::now)
: tr::lng_context_spoiler_effect(tr::now), [=] {
setSpoiler(!hasSpoiler());
}, &st::menuIconCopy);
if (_menu->empty()) {
_menu = nullptr;
} else {
_menu->popup(position);
}
}
int AbstractSingleMediaPreview::previewLeft() const {
return _previewLeft;
}

View file

@ -9,27 +9,34 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/chat/attach/attach_abstract_single_preview.h"
#include "ui/chat/attach/attach_controls.h"
#include "ui/chat/attach/attach_send_files_way.h"
#include "ui/abstract_button.h"
namespace Ui {
class PopupMenu;
class AbstractSingleMediaPreview : public AbstractSinglePreview {
public:
AbstractSingleMediaPreview(QWidget *parent, AttachControls::Type type);
~AbstractSingleMediaPreview();
void setSendWay(SendFilesWay way);
[[nodiscard]] rpl::producer<> deleteRequests() const override;
[[nodiscard]] rpl::producer<> editRequests() const override;
[[nodiscard]] rpl::producer<> modifyRequests() const override;
[[nodiscard]] bool isPhoto() const;
void setSpoiler(bool spoiler);
[[nodiscard]] bool hasSpoiler() const;
protected:
virtual bool drawBackground() const = 0;
virtual bool tryPaintAnimation(QPainter &p) = 0;
virtual bool isAnimatedPreviewReady() const = 0;
void updatePhotoEditorButton();
void preparePreview(QImage preview);
int previewLeft() const;
@ -42,17 +49,33 @@ protected:
private:
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
[[nodiscard]] bool isOverPreview(QPoint position) const;
void applyCursor(style::cursor cursor);
void showContextMenu(QPoint position);
SendFilesWay _sendWay;
bool _animated = false;
QPixmap _preview;
QPixmap _previewBlurred;
int _previewLeft = 0;
int _previewTop = 0;
int _previewWidth = 0;
int _previewHeight = 0;
std::unique_ptr<SpoilerAnimation> _spoiler;
const int _minThumbH;
const base::unique_qptr<AbstractButton> _photoEditorButton;
const base::unique_qptr<AttachControlsWidget> _controls;
rpl::event_stream<> _photoEditorRequests;
style::cursor _cursor = style::cur_default;
bool _pressed = false;
base::unique_qptr<PopupMenu> _menu;
rpl::event_stream<> _modifyRequests;

View file

@ -9,10 +9,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/chat/attach/attach_album_thumbnail.h"
#include "ui/chat/attach/attach_prepare.h"
#include "ui/widgets/popup_menu.h"
#include "ui/painter.h"
#include "lang/lang_keys.h"
#include "styles/style_chat.h"
#include "styles/style_boxes.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
#include <QtWidgets/QApplication>
@ -61,6 +64,19 @@ void AlbumPreview::updateFileRows() {
}
}
base::flat_set<int> AlbumPreview::collectSpoileredIndices() {
auto result = base::flat_set<int>();
result.reserve(_thumbs.size());
auto i = 0;
for (const auto &thumb : _thumbs) {
if (thumb->hasSpoiler()) {
result.emplace(i);
}
++i;
}
return result;
}
std::vector<int> AlbumPreview::takeOrder() {
//Expects(_thumbs.size() == _order.size());
//Expects(_itemsShownDimensions.size() == _order.size());
@ -112,6 +128,7 @@ void AlbumPreview::prepareThumbs(gsl::span<Ui::PreparedFile> items) {
items[i],
layout[i],
this,
[=] { update(); },
[=] { changeThumbByIndex(thumbIndex(thumbUnderCursor())); },
[=] { deleteThumbByIndex(thumbIndex(thumbUnderCursor())); }));
if (_thumbs.back()->isCompressedSticker()) {
@ -365,11 +382,7 @@ void AlbumPreview::paintFiles(Painter &p, QRect clip) const {
} else if (bottom <= clip.y()) {
continue;
}
if (thumb->isCompressedSticker()) {
thumb->paintPhoto(p, left, top, outerWidth);
} else {
thumb->paintFile(p, left, top, outerWidth);
}
thumb->paintFile(p, left, top, outerWidth);
}
}
}
@ -393,15 +406,6 @@ void AlbumPreview::deleteThumbByIndex(int index) {
if (index < 0) {
return;
}
const auto orderIt = ranges::find(_order, index);
Expects(orderIt != _order.end());
_order.erase(orderIt);
ranges::for_each(_order, [=](auto &i) {
if (i > index) {
i--;
}
});
_thumbDeleted.fire(std::move(index));
}
@ -448,7 +452,8 @@ void AlbumPreview::mousePressEvent(QMouseEvent *e) {
const auto isAlbum = _sendWay.sendImagesAsPhotos()
&& _sendWay.groupFiles();
if (!isAlbum) {
if (!isAlbum || e->button() != Qt::LeftButton) {
_dragTimer.cancel();
return;
}
@ -554,13 +559,38 @@ void AlbumPreview::mouseReleaseEvent(QMouseEvent *e) {
} else if (const auto thumb = base::take(_pressedThumb)) {
const auto was = _pressedButtonType;
const auto now = thumb->buttonTypeFromPoint(e->pos());
if (was == now) {
if (e->button() == Qt::RightButton) {
showContextMenu(thumb, e->globalPos());
} else if (was == now) {
thumbButtonsCallback(thumb, now);
}
}
_pressedButtonType = AttachButtonType::None;
}
void AlbumPreview::showContextMenu(
not_null<AlbumThumbnail*> thumb,
QPoint position) {
if (!_sendWay.sendImagesAsPhotos()) {
return;
}
_menu = base::make_unique_q<Ui::PopupMenu>(
this,
st::popupMenuWithIcons);
_menu->addAction(thumb->hasSpoiler()
? tr::lng_context_disable_spoiler(tr::now)
: tr::lng_context_spoiler_effect(tr::now), [=] {
thumb->setSpoiler(!thumb->hasSpoiler());
}, &st::menuIconCopy);
if (_menu->empty()) {
_menu = nullptr;
} else {
_menu->popup(position);
}
}
void AlbumPreview::switchToDrag() {
_paintedAbove
= _suggestedThumb

View file

@ -16,6 +16,7 @@ namespace Ui {
struct PreparedFile;
struct GroupMediaLayout;
class AlbumThumbnail;
class PopupMenu;
class AlbumPreview final : public RpWidget {
public:
@ -26,7 +27,9 @@ public:
~AlbumPreview();
void setSendWay(SendFilesWay way);
std::vector<int> takeOrder();
[[nodiscard]] base::flat_set<int> collectSpoileredIndices();
[[nodiscard]] std::vector<int> takeOrder();
[[nodiscard]] rpl::producer<int> thumbDeleted() const {
return _thumbDeleted.events();
@ -79,6 +82,8 @@ private:
void cancelDrag();
void finishDrag();
void showContextMenu(not_null<AlbumThumbnail*> thumb, QPoint position);
SendFilesWay _sendWay;
style::cursor _cursor = style::cur_default;
std::vector<int> _order;
@ -103,6 +108,8 @@ private:
rpl::event_stream<int> _thumbChanged;
rpl::event_stream<int> _thumbModified;
base::unique_qptr<PopupMenu> _menu;
mutable Animations::Simple _thumbsHeightAnimation;
mutable Animations::Simple _shrinkAnimation;
mutable Animations::Simple _finishDragAnimation;

View file

@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/image/image_prepare.h"
#include "ui/text/format_values.h"
#include "ui/widgets/buttons.h"
#include "ui/effects/spoiler_mess.h"
#include "ui/ui_utility.h"
#include "ui/painter.h"
#include "base/call_delayed.h"
@ -26,6 +27,7 @@ AlbumThumbnail::AlbumThumbnail(
const PreparedFile &file,
const GroupMediaLayout &layout,
QWidget *parent,
Fn<void()> repaint,
Fn<void()> editCallback,
Fn<void()> deleteCallback)
: _layout(layout)
@ -33,7 +35,8 @@ AlbumThumbnail::AlbumThumbnail(
, _shrinkSize(int(std::ceil(st::roundRadiusLarge / 1.4)))
, _isPhoto(file.type == PreparedFile::Type::Photo)
, _isVideo(file.type == PreparedFile::Type::Video)
, _isCompressedSticker(Core::IsMimeSticker(file.information->filemime)) {
, _isCompressedSticker(Core::IsMimeSticker(file.information->filemime))
, _repaint(std::move(repaint)) {
Expects(!_fullPreview.isNull());
moveToLayout(layout);
@ -107,9 +110,23 @@ AlbumThumbnail::AlbumThumbnail(
_editMedia->setIconOverride(&st::sendBoxAlbumGroupEditButtonIconFile);
_deleteMedia->setIconOverride(&st::sendBoxAlbumGroupDeleteButtonIconFile);
setSpoiler(file.spoiler);
setButtonVisible(false);
}
void AlbumThumbnail::setSpoiler(bool spoiler) {
Expects(_repaint != nullptr);
_spoiler = spoiler
? std::make_unique<SpoilerAnimation>(_repaint)
: nullptr;
_repaint();
}
bool AlbumThumbnail::hasSpoiler() const {
return _spoiler != nullptr;
}
void AlbumThumbnail::setButtonVisible(bool value) {
_editMedia->setVisible(value);
_deleteMedia->setVisible(value);
@ -135,7 +152,7 @@ void AlbumThumbnail::animateLayoutToInitial() {
}
void AlbumThumbnail::moveToLayout(const GroupMediaLayout &layout) {
using Option = Images::Option;
using namespace Images;
animateLayoutToInitial();
_layout = layout;
@ -143,27 +160,20 @@ void AlbumThumbnail::moveToLayout(const GroupMediaLayout &layout) {
const auto width = _layout.geometry.width();
const auto height = _layout.geometry.height();
_albumCorners = GetCornersFromSides(_layout.sides);
const auto corner = [&](RectPart part, Option skip) {
return !(_albumCorners & part) ? skip : Option();
};
const auto options = Option::RoundLarge
| corner(RectPart::TopLeft, Option::RoundSkipTopLeft)
| corner(RectPart::TopRight, Option::RoundSkipTopRight)
| corner(RectPart::BottomLeft, Option::RoundSkipBottomLeft)
| corner(RectPart::BottomRight, Option::RoundSkipBottomRight);
const auto pixSize = GetImageScaleSizeForGeometry(
{ _fullPreview.width(), _fullPreview.height() },
{ width, height });
const auto pixWidth = pixSize.width() * style::DevicePixelRatio();
const auto pixHeight = pixSize.height() * style::DevicePixelRatio();
_albumImage = PixmapFromImage(Images::Prepare(
_albumImage = PixmapFromImage(Prepare(
_fullPreview,
QSize(pixWidth, pixHeight),
{
.options = options,
.options = RoundOptions(ImageRoundRadius::Large, _albumCorners),
.outer = { width, height },
}));
_albumImageBlurred = QPixmap();
}
int AlbumThumbnail::photoHeight() const {
@ -188,46 +198,83 @@ void AlbumThumbnail::paintInAlbum(
float64 moveProgress) {
const auto shrink = anim::interpolate(0, _shrinkSize, shrinkProgress);
_lastShrinkValue = shrink;
const auto geometry = countCurrentGeometry(moveProgress);
const auto x = left + geometry.x();
const auto y = top + geometry.y();
if (shrink > 0 || moveProgress < 1.) {
const auto size = geometry.size();
if (shrinkProgress < 1 && _albumCorners != RectPart::None) {
prepareCache(size, shrink);
p.drawImage(x, y, _albumCache);
} else {
const auto to = QRect({ x, y }, size).marginsRemoved(
const auto geometry = countCurrentGeometry(
moveProgress
).translated(left, top);
auto paintedTo = geometry;
const auto revealed = _spoiler ? shrinkProgress : 1.;
if (revealed > 0.) {
if (shrink > 0 || moveProgress < 1.) {
const auto size = geometry.size();
paintedTo = geometry.marginsRemoved(
{ shrink, shrink, shrink, shrink }
);
drawSimpleFrame(p, to, size);
if (shrinkProgress < 1 && _albumCorners != RectPart::None) {
prepareCache(size, shrink);
p.drawImage(geometry.topLeft(), _albumCache);
} else {
drawSimpleFrame(p, paintedTo, size);
}
} else {
p.drawPixmap(geometry.topLeft(), _albumImage);
}
if (_isVideo) {
paintPlayVideo(p, geometry);
}
} else {
p.drawPixmap(x, y, _albumImage);
}
if (_isVideo) {
const auto innerSize = st::msgFileLayout.thumbSize;
const auto inner = QRect(
x + (geometry.width() - innerSize) / 2,
y + (geometry.height() - innerSize) / 2,
innerSize,
innerSize);
{
PainterHighQualityEnabler hq(p);
p.setPen(Qt::NoPen);
p.setBrush(st::msgDateImgBg);
p.drawEllipse(inner);
if (revealed < 1.) {
auto corners = Images::CornersMaskRef(
Images::CornersMask(ImageRoundRadius::Large));
if (!(_albumCorners & RectPart::TopLeft)) {
corners.p[0] = nullptr;
}
st::historyFileThumbPlay.paintInCenter(p, inner);
if (!(_albumCorners & RectPart::TopRight)) {
corners.p[1] = nullptr;
}
if (!(_albumCorners & RectPart::BottomLeft)) {
corners.p[2] = nullptr;
}
if (!(_albumCorners & RectPart::BottomRight)) {
corners.p[3] = nullptr;
}
p.setOpacity(1. - revealed);
if (_albumImageBlurred.isNull()) {
_albumImageBlurred = BlurredPreviewFromPixmap(
_albumImage,
_albumCorners);
}
p.drawPixmap(paintedTo, _albumImageBlurred);
FillSpoilerRect(
p,
paintedTo,
corners,
DefaultImageSpoiler().frame(_spoiler->index(crl::now(), false)),
_cornerCache);
p.setOpacity(1.);
}
_lastRectOfButtons = paintButtons(
p,
{ x, y },
geometry.topLeft(),
geometry.width(),
shrinkProgress);
_lastRectOfModify = geometry;
}
_lastRectOfModify = QRect(QPoint(x, y), geometry.size());
void AlbumThumbnail::paintPlayVideo(QPainter &p, QRect geometry) {
const auto innerSize = st::msgFileLayout.thumbSize;
const auto inner = QRect(
geometry.x() + (geometry.width() - innerSize) / 2,
geometry.y() + (geometry.height() - innerSize) / 2,
innerSize,
innerSize);
{
PainterHighQualityEnabler hq(p);
p.setPen(Qt::NoPen);
p.setBrush(st::msgDateImgBg);
p.drawEllipse(inner);
}
st::historyFileThumbPlay.paintInCenter(p, inner);
}
void AlbumThumbnail::prepareCache(QSize size, int shrink) {
@ -376,11 +423,33 @@ void AlbumThumbnail::drawSimpleFrame(QPainter &p, QRect to, QSize size) const {
void AlbumThumbnail::paintPhoto(Painter &p, int left, int top, int outerWidth) {
const auto size = _photo.size() / style::DevicePixelRatio();
if (_spoiler && _photoBlurred.isNull()) {
_photoBlurred = BlurredPreviewFromPixmap(
_photo,
RectPart::AllCorners);
}
const auto &pixmap = _spoiler ? _photoBlurred : _photo;
const auto rect = QRect(
left + (st::sendMediaPreviewSize - size.width()) / 2,
top,
pixmap.width() / pixmap.devicePixelRatio(),
pixmap.height() / pixmap.devicePixelRatio());
p.drawPixmapLeft(
left + (st::sendMediaPreviewSize - size.width()) / 2,
top,
outerWidth,
_photo);
pixmap);
if (_spoiler) {
FillSpoilerRect(
p,
rect,
Images::CornersMaskRef(
Images::CornersMask(ImageRoundRadius::Large)),
DefaultImageSpoiler().frame(_spoiler->index(crl::now(), false)),
_cornerCache);
} else if (_isVideo) {
paintPlayVideo(p, rect);
}
const auto topLeft = QPoint{ left, top };
@ -398,6 +467,13 @@ void AlbumThumbnail::paintFile(
int left,
int top,
int outerWidth) {
if (isCompressedSticker()) {
auto spoiler = base::take(_spoiler);
paintPhoto(p, left, top, outerWidth);
_spoiler = base::take(spoiler);
return;
}
const auto &st = st::attachPreviewThumbLayout;
const auto textLeft = left + st.thumbSize + st.thumbSkip;

View file

@ -18,6 +18,7 @@ namespace Ui {
struct PreparedFile;
class IconButton;
class SpoilerAnimation;
class AlbumThumbnail final {
public:
@ -25,6 +26,7 @@ public:
const PreparedFile &file,
const GroupMediaLayout &layout,
QWidget *parent,
Fn<void()> repaint,
Fn<void()> editCallback,
Fn<void()> deleteCallback);
@ -32,6 +34,9 @@ public:
void animateLayoutToInitial();
void resetLayoutAnimation();
void setSpoiler(bool spoiler);
[[nodiscard]] bool hasSpoiler() const;
int photoHeight() const;
int fileHeight() const;
@ -71,6 +76,7 @@ private:
QPoint point,
int outerWidth,
float64 shrinkProgress);
void paintPlayVideo(QPainter &p, QRect geometry);
GroupMediaLayout _layout;
std::optional<QRect> _animateFromGeometry;
@ -79,10 +85,12 @@ private:
const bool _isPhoto;
const bool _isVideo;
QPixmap _albumImage;
QPixmap _albumImageBlurred;
QImage _albumCache;
QPoint _albumPosition;
RectParts _albumCorners = RectPart::None;
QPixmap _photo;
QPixmap _photoBlurred;
QPixmap _fileThumb;
QString _name;
QString _status;
@ -94,6 +102,9 @@ private:
AttachControls _buttons;
bool _isCompressedSticker = false;
std::unique_ptr<SpoilerAnimation> _spoiler;
QImage _cornerCache;
Fn<void()> _repaint;
QRect _lastRectOfModify;
QRect _lastRectOfButtons;

View file

@ -295,4 +295,28 @@ QPixmap PrepareSongCoverForThumbnail(QImage image, int size) {
}));
}
QPixmap BlurredPreviewFromPixmap(QPixmap pixmap, RectParts corners) {
const auto image = pixmap.toImage();
const auto skip = st::roundRadiusLarge * image.devicePixelRatio();
auto small = image.copy(
skip,
skip,
image.width() - 2 * skip,
image.height() - 2 * skip
).scaled(
40,
40,
Qt::KeepAspectRatioByExpanding,
Qt::SmoothTransformation);
using namespace Images;
return PixmapFromImage(Prepare(
Blur(std::move(small), true),
image.size() / style::DevicePixelRatio(),
{
.options = RoundOptions(ImageRoundRadius::Large, corners),
.outer = image.size() / style::DevicePixelRatio(),
}));
}
} // namespace Ui

View file

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "editor/photo_editor_common.h"
#include "ui/rect_part.h"
#include <QtCore/QSemaphore>
#include <deque>
@ -80,6 +81,7 @@ struct PreparedFile {
QImage preview;
QSize shownDimensions;
Type type = Type::File;
bool spoiler = false;
};
[[nodiscard]] bool CanBeInAlbumType(PreparedFile::Type type, AlbumType album);
@ -143,4 +145,8 @@ struct PreparedGroup {
[[nodiscard]] QPixmap PrepareSongCoverForThumbnail(QImage image, int size);
[[nodiscard]] QPixmap BlurredPreviewFromPixmap(
QPixmap pixmap,
RectParts corners);
} // namespace Ui

View file

@ -47,6 +47,7 @@ SingleMediaPreview *SingleMediaPreview::Create(
preview,
animated,
Core::IsMimeSticker(file.information->filemime),
file.spoiler,
animationPreview ? file.path : QString(),
type);
}
@ -57,6 +58,7 @@ SingleMediaPreview::SingleMediaPreview(
QImage preview,
bool animated,
bool sticker,
bool spoiler,
const QString &animatedPreviewPath,
AttachControls::Type type)
: AbstractSingleMediaPreview(parent, type)
@ -67,7 +69,7 @@ SingleMediaPreview::SingleMediaPreview(
preparePreview(preview);
prepareAnimatedPreview(animatedPreviewPath, animated);
updatePhotoEditorButton();
setSpoiler(spoiler);
}
bool SingleMediaPreview::drawBackground() const {

View file

@ -32,6 +32,7 @@ public:
QImage preview,
bool animated,
bool sticker,
bool spoiler,
const QString &animatedPreviewPath,
AttachControls::Type type);