init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,205 @@
/*
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 "ui/chat/attach/attach_abstract_single_file_preview.h"
#include "base/timer_rpl.h"
#include "ui/image/image_prepare.h"
#include "ui/painter.h"
#include "ui/text/text_options.h"
#include "ui/ui_utility.h"
#include "ui/widgets/buttons.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
namespace Ui {
AbstractSingleFilePreview::AbstractSingleFilePreview(
QWidget *parent,
const style::ComposeControls &st,
AttachControls::Type type)
: AbstractSinglePreview(parent)
, _st(st)
, _type(type)
, _editMedia(this, _st.files.buttonFile)
, _deleteMedia(this, _st.files.buttonFile) {
_editMedia->setIconOverride(&_st.files.buttonFileEdit);
_deleteMedia->setIconOverride(&_st.files.buttonFileDelete);
if (type == AttachControls::Type::Full) {
_deleteMedia->show();
_editMedia->show();
} else if (type == AttachControls::Type::EditOnly) {
_deleteMedia->hide();
_editMedia->show();
} else if (type == AttachControls::Type::None) {
_deleteMedia->hide();
_editMedia->hide();
}
}
AbstractSingleFilePreview::~AbstractSingleFilePreview() = default;
rpl::producer<> AbstractSingleFilePreview::editRequests() const {
return _editMedia->clicks() | rpl::map([] {
return base::timer_once(st::historyAttach.ripple.hideDuration);
}) | rpl::flatten_latest();
}
rpl::producer<> AbstractSingleFilePreview::deleteRequests() const {
return _deleteMedia->clicks() | rpl::to_empty;
}
rpl::producer<> AbstractSingleFilePreview::modifyRequests() const {
return rpl::never<>();
}
rpl::producer<> AbstractSingleFilePreview::editCoverRequests() const {
return rpl::never<>();
}
rpl::producer<> AbstractSingleFilePreview::clearCoverRequests() const {
return rpl::never<>();
}
void AbstractSingleFilePreview::prepareThumbFor(
Data &data,
const QImage &preview) {
if (preview.isNull()) {
return;
}
auto originalWidth = preview.width();
auto originalHeight = preview.height();
const auto &st = st::attachPreviewThumbLayout;
auto thumbWidth = st.thumbSize;
if (originalWidth > originalHeight) {
thumbWidth = (originalWidth * st.thumbSize) / originalHeight;
}
const auto options = Images::Option::RoundSmall;
data.fileThumb = PixmapFromImage(Images::Prepare(
preview,
thumbWidth * style::DevicePixelRatio(),
{ .options = options, .outer = { st.thumbSize, st.thumbSize } }));
}
void AbstractSingleFilePreview::paintEvent(QPaintEvent *e) {
Painter p(this);
const auto w = width()
- st::boxPhotoPadding.left()
- st::boxPhotoPadding.right();
const auto &st = !isThumbedLayout(_data)
? st::attachPreviewLayout
: st::attachPreviewThumbLayout;
const auto nameleft = st.thumbSize + st.thumbSkip;
const auto nametop = st.nameTop;
const auto statustop = st.statusTop;
const auto x = (width() - w) / 2, y = 0;
if (!isThumbedLayout(_data)) {
QRect inner(
style::rtlrect(x, y, st.thumbSize, st.thumbSize, width()));
p.setPen(Qt::NoPen);
if (_data.fileIsAudio && !_data.fileThumb.isNull()) {
p.drawPixmap(inner.topLeft(), _data.fileThumb);
} else {
p.setBrush(_st.files.iconBg);
PainterHighQualityEnabler hq(p);
p.drawEllipse(inner);
}
auto &icon = _data.fileIsAudio
? (_data.fileThumb.isNull()
? _st.files.iconPlay
: st::historyFileThumbPlay)
: _data.fileIsImage
? _st.files.iconImage
: _st.files.iconDocument;
icon.paintInCenter(p, inner);
} else {
QRect rthumb(
style::rtlrect(x, y, st.thumbSize, st.thumbSize, width()));
p.drawPixmap(rthumb.topLeft(), _data.fileThumb);
}
p.setFont(st::semiboldFont);
p.setPen(_st.files.nameFg);
p.drawTextLeft(
x + nameleft,
y + nametop, width(),
_data.name,
_data.nameWidth);
p.setFont(st::normalFont);
p.setPen(_st.files.statusFg);
p.drawTextLeft(
x + nameleft,
y + statustop,
width(),
_data.statusText,
_data.statusWidth);
}
void AbstractSingleFilePreview::resizeEvent(QResizeEvent *e) {
const auto w = width()
- st::boxPhotoPadding.left()
- st::boxPhotoPadding.right();
const auto x = (width() - w) / 2;
const auto top = st::sendBoxFileGroupSkipTop;
auto right = st::sendBoxFileGroupSkipRight + x;
if (_type != AttachControls::Type::EditOnly) {
_deleteMedia->moveToRight(right, top);
right += st::sendBoxFileGroupEditInternalSkip + _deleteMedia->width();
}
_editMedia->moveToRight(right, top);
}
bool AbstractSingleFilePreview::isThumbedLayout(Data &data) const {
return (!data.fileThumb.isNull() && !data.fileIsAudio);
}
void AbstractSingleFilePreview::updateTextWidthFor(Data &data) {
const auto &st = !isThumbedLayout(data)
? st::attachPreviewLayout
: st::attachPreviewThumbLayout;
const auto buttonsCount = (_type == AttachControls::Type::EditOnly)
? 1
: (_type == AttachControls::Type::Full)
? 2
: 0;
const auto availableFileWidth = st::sendMediaPreviewSize
- st.thumbSize
- st.thumbSkip
// Right buttons.
- _st.files.buttonFile.width * buttonsCount
- st::sendBoxAlbumGroupEditInternalSkip * buttonsCount
- st::sendBoxAlbumGroupSkipRight;
data.nameWidth = st::semiboldFont->width(data.name);
if (data.nameWidth > availableFileWidth) {
data.name = st::semiboldFont->elided(
data.name,
availableFileWidth,
Qt::ElideMiddle);
data.nameWidth = st::semiboldFont->width(data.name);
}
data.statusWidth = st::normalFont->width(data.statusText);
}
void AbstractSingleFilePreview::setData(const Data &data) {
_data = data;
updateTextWidthFor(_data);
const auto &st = !isThumbedLayout(_data)
? st::attachPreviewLayout
: st::attachPreviewThumbLayout;
resize(width(), st.thumbSize);
}
} // namespace Ui

View File

@@ -0,0 +1,68 @@
/*
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/attach/attach_abstract_single_preview.h"
#include "ui/chat/attach/attach_controls.h"
#include "base/object_ptr.h"
namespace style {
struct ComposeControls;
} // namespace style
namespace Ui {
class IconButton;
class AbstractSingleFilePreview : public AbstractSinglePreview {
public:
AbstractSingleFilePreview(
QWidget *parent,
const style::ComposeControls &st,
AttachControls::Type type);
~AbstractSingleFilePreview();
[[nodiscard]] rpl::producer<> deleteRequests() const override;
[[nodiscard]] rpl::producer<> editRequests() const override;
[[nodiscard]] rpl::producer<> modifyRequests() const override;
[[nodiscard]] rpl::producer<> editCoverRequests() const override;
[[nodiscard]] rpl::producer<> clearCoverRequests() const override;
protected:
struct Data {
QPixmap fileThumb;
QString name;
QString statusText;
int nameWidth = 0;
int statusWidth = 0;
bool fileIsAudio = false;
bool fileIsImage = false;
};
void prepareThumbFor(Data &data, const QImage &preview);
bool isThumbedLayout(Data &data) const;
void setData(const Data &data);
private:
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void updateTextWidthFor(Data &data);
const style::ComposeControls &_st;
const AttachControls::Type _type;
Data _data;
object_ptr<IconButton> _editMedia = { nullptr };
object_ptr<IconButton> _deleteMedia = { nullptr };
};
} // namespace Ui

View File

@@ -0,0 +1,344 @@
/*
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 "ui/chat/attach/attach_abstract_single_media_preview.h"
#include "editor/photo_editor_common.h"
#include "lang/lang_keys.h"
#include "ui/chat/attach/attach_controls.h"
#include "ui/chat/attach/attach_prepare.h"
#include "ui/effects/spoiler_mess.h"
#include "ui/image/image_prepare.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "ui/ui_utility.h"
#include "ui/widgets/popup_menu.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
namespace Ui {
namespace {
constexpr auto kMinPreviewWidth = 20;
} // namespace
AbstractSingleMediaPreview::AbstractSingleMediaPreview(
QWidget *parent,
const style::ComposeControls &st,
AttachControls::Type type,
Fn<bool(AttachActionType)> actionAllowed)
: AbstractSinglePreview(parent)
, _st(st)
, _actionAllowed(std::move(actionAllowed))
, _minThumbH(st::sendBoxAlbumGroupSize.height()
+ st::sendBoxAlbumGroupSkipTop * 2)
, _controls(base::make_unique_q<AttachControlsWidget>(this, type)) {
}
AbstractSingleMediaPreview::~AbstractSingleMediaPreview() = default;
rpl::producer<> AbstractSingleMediaPreview::deleteRequests() const {
return _controls->deleteRequests();
}
rpl::producer<> AbstractSingleMediaPreview::editRequests() const {
return _controls->editRequests();
}
rpl::producer<> AbstractSingleMediaPreview::modifyRequests() const {
return _photoEditorRequests.events();
}
rpl::producer<> AbstractSingleMediaPreview::editCoverRequests() const {
return _editCoverRequests.events();
}
rpl::producer<> AbstractSingleMediaPreview::clearCoverRequests() const {
return _clearCoverRequests.events();
}
void AbstractSingleMediaPreview::setSendWay(SendFilesWay way) {
_sendWay = way;
update();
}
SendFilesWay AbstractSingleMediaPreview::sendWay() const {
return _sendWay;
}
void AbstractSingleMediaPreview::setSpoiler(bool spoiler) {
_spoiler = spoiler
? std::make_unique<SpoilerAnimation>([=] { update(); })
: nullptr;
update();
}
bool AbstractSingleMediaPreview::hasSpoiler() const {
return _spoiler != nullptr;
}
bool AbstractSingleMediaPreview::canHaveSpoiler() const {
return supportsSpoilers();
}
rpl::producer<bool> AbstractSingleMediaPreview::spoileredChanges() const {
return _spoileredChanges.events();
}
QImage AbstractSingleMediaPreview::generatePriceTagBackground() const {
return (_previewBlurred.isNull() ? _preview : _previewBlurred).toImage();
}
void AbstractSingleMediaPreview::preparePreview(QImage preview) {
auto maxW = 0;
auto maxH = 0;
if (_animated && drawBackground()) {
auto limitW = st::sendMediaPreviewSize;
auto limitH = st::confirmMaxHeight;
maxW = qMax(preview.width(), 1);
maxH = qMax(preview.height(), 1);
if (maxW * limitH > maxH * limitW) {
if (maxW < limitW) {
maxH = maxH * limitW / maxW;
maxW = limitW;
}
} else {
if (maxH < limitH) {
maxW = maxW * limitH / maxH;
maxH = limitH;
}
}
const auto ratio = style::DevicePixelRatio();
preview = Images::Prepare(
std::move(preview),
QSize(maxW, maxH) * ratio,
{ .outer = { maxW, maxH } });
}
auto originalWidth = preview.width();
auto originalHeight = preview.height();
if (!originalWidth || !originalHeight) {
originalWidth = originalHeight = 1;
}
_previewWidth = st::sendMediaPreviewSize;
if (preview.width() < _previewWidth) {
_previewWidth = qMax(preview.width(), kMinPreviewWidth);
}
auto maxthumbh = qMin(qRound(1.5 * _previewWidth), st::confirmMaxHeight);
_previewHeight = qRound(originalHeight
* float64(_previewWidth)
/ originalWidth);
if (_previewHeight > maxthumbh) {
_previewWidth = qRound(_previewWidth
* float64(maxthumbh)
/ _previewHeight);
accumulate_max(_previewWidth, kMinPreviewWidth);
_previewHeight = maxthumbh;
}
_previewLeft = (st::boxWideWidth - _previewWidth) / 2;
if (_previewHeight < _minThumbH) {
_previewTop = (_minThumbH - _previewHeight) / 2;
}
preview = std::move(preview).scaled(
_previewWidth * style::DevicePixelRatio(),
_previewHeight * style::DevicePixelRatio(),
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
preview = Images::Opaque(std::move(preview));
_preview = PixmapFromImage(std::move(preview));
_preview.setDevicePixelRatio(style::DevicePixelRatio());
_previewBlurred = QPixmap();
resize(width(), std::max(_previewHeight, _minThumbH));
}
bool AbstractSingleMediaPreview::isOverPreview(QPoint position) const {
return QRect(
_previewLeft,
_previewTop,
_previewWidth,
_previewHeight).contains(position);
}
void AbstractSingleMediaPreview::resizeEvent(QResizeEvent *e) {
_controls->moveToRight(
st::boxPhotoPadding.right() + st::sendBoxAlbumGroupSkipRight,
st::sendBoxAlbumGroupSkipTop,
width());
}
void AbstractSingleMediaPreview::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
auto takenSpoiler = supportsSpoilers()
? 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()) {
p.fillRect(
padding.left(),
_previewTop,
_previewLeft - padding.left(),
_previewHeight,
_st.files.confirmBg);
}
if ((_previewLeft + _previewWidth) < (width() - padding.right())) {
p.fillRect(
_previewLeft + _previewWidth,
_previewTop,
width() - padding.right() - _previewLeft - _previewWidth,
_previewHeight,
_st.files.confirmBg);
}
if (_previewTop > 0) {
p.fillRect(
padding.left(),
0,
width() - padding.right() - padding.left(),
height(),
_st.files.confirmBg);
}
}
if (_spoiler && _previewBlurred.isNull()) {
_previewBlurred = BlurredPreviewFromPixmap(_preview, RectPart::None);
}
if (_spoiler || !tryPaintAnimation(p)) {
const auto &pixmap = _spoiler ? _previewBlurred : _preview;
const auto position = QPoint(_previewLeft, _previewTop);
p.drawPixmap(position, pixmap);
if (_spoiler) {
const auto paused = On(PowerSaving::kChatSpoiler);
FillSpoilerRect(
p,
QRect(position, pixmap.size() / pixmap.devicePixelRatio()),
DefaultImageSpoiler().frame(
_spoiler->index(crl::now(), paused)));
}
}
if (_animated && !isAnimatedPreviewReady() && !_spoiler) {
const auto innerSize = st::msgFileLayout.thumbSize;
auto inner = QRect(
_previewLeft + (_previewWidth - innerSize) / 2,
_previewTop + (_previewHeight - innerSize) / 2,
innerSize,
innerSize);
p.setPen(Qt::NoPen);
p.setBrush(st::msgDateImgBg);
{
PainterHighQualityEnabler hq(p);
p.drawEllipse(inner);
}
auto icon = &st::historyFileInPlay;
icon->paintInCenter(p, inner);
}
}
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) {
_menu = base::make_unique_q<Ui::PopupMenu>(
this,
_st.tabbed.menu);
const auto &icons = _st.tabbed.icons;
if (_actionAllowed(AttachActionType::ToggleSpoiler)
&& _sendWay.sendImagesAsPhotos()
&& supportsSpoilers()) {
const auto spoilered = hasSpoiler();
_menu->addAction(spoilered
? tr::lng_context_disable_spoiler(tr::now)
: tr::lng_context_spoiler_effect(tr::now), [=] {
setSpoiler(!spoilered);
_spoileredChanges.fire_copy(!spoilered);
}, spoilered ? &icons.menuSpoilerOff : &icons.menuSpoiler);
}
if (_actionAllowed(AttachActionType::EditCover)) {
_menu->addAction(tr::lng_context_edit_cover(tr::now), [=] {
_editCoverRequests.fire({});
}, &st::menuIconEdit);
if (_actionAllowed(AttachActionType::ClearCover)) {
_menu->addAction(tr::lng_context_clear_cover(tr::now), [=] {
_clearCoverRequests.fire({});
}, &st::menuIconCancel);
}
}
if (_menu->empty()) {
_menu = nullptr;
} else {
_menu->popup(position);
}
}
int AbstractSingleMediaPreview::previewLeft() const {
return _previewLeft;
}
int AbstractSingleMediaPreview::previewTop() const {
return _previewTop;
}
int AbstractSingleMediaPreview::previewWidth() const {
return _previewWidth;
}
int AbstractSingleMediaPreview::previewHeight() const {
return _previewHeight;
}
void AbstractSingleMediaPreview::setAnimated(bool animated) {
_animated = animated;
}
bool AbstractSingleMediaPreview::isPhoto() const {
return drawBackground()
&& !isAnimatedPreviewReady()
&& !_animated;
}
} // namespace Ui

View File

@@ -0,0 +1,106 @@
/*
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/attach/attach_abstract_single_preview.h"
#include "ui/chat/attach/attach_controls.h"
#include "ui/chat/attach/attach_send_files_way.h"
#include "ui/effects/spoiler_mess.h"
#include "ui/abstract_button.h"
namespace style {
struct ComposeControls;
} // namespace style
namespace Ui {
class PopupMenu;
class AbstractSingleMediaPreview : public AbstractSinglePreview {
public:
AbstractSingleMediaPreview(
QWidget *parent,
const style::ComposeControls &st,
AttachControls::Type type,
Fn<bool(AttachActionType)> actionAllowed);
~AbstractSingleMediaPreview();
void setSendWay(SendFilesWay way);
[[nodiscard]] SendFilesWay sendWay() const;
[[nodiscard]] rpl::producer<> deleteRequests() const override;
[[nodiscard]] rpl::producer<> editRequests() const override;
[[nodiscard]] rpl::producer<> modifyRequests() const override;
[[nodiscard]] rpl::producer<> editCoverRequests() const override;
[[nodiscard]] rpl::producer<> clearCoverRequests() const override;
[[nodiscard]] bool isPhoto() const;
void setSpoiler(bool spoiler);
[[nodiscard]] bool hasSpoiler() const;
[[nodiscard]] bool canHaveSpoiler() const;
[[nodiscard]] rpl::producer<bool> spoileredChanges() const;
[[nodiscard]] QImage generatePriceTagBackground() const;
protected:
virtual bool supportsSpoilers() const = 0;
virtual bool drawBackground() const = 0;
virtual bool tryPaintAnimation(QPainter &p) = 0;
virtual bool isAnimatedPreviewReady() const = 0;
void preparePreview(QImage preview);
int previewLeft() const;
int previewTop() const;
int previewWidth() const;
int previewHeight() const;
void setAnimated(bool animated);
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);
const style::ComposeControls &_st;
SendFilesWay _sendWay;
Fn<bool(AttachActionType)> _actionAllowed;
bool _animated = false;
QPixmap _preview;
QPixmap _previewBlurred;
int _previewLeft = 0;
int _previewTop = 0;
int _previewWidth = 0;
int _previewHeight = 0;
std::unique_ptr<SpoilerAnimation> _spoiler;
rpl::event_stream<bool> _spoileredChanges;
const int _minThumbH;
const base::unique_qptr<AttachControlsWidget> _controls;
rpl::event_stream<> _photoEditorRequests;
rpl::event_stream<> _editCoverRequests;
rpl::event_stream<> _clearCoverRequests;
style::cursor _cursor = style::cur_default;
bool _pressed = false;
base::unique_qptr<PopupMenu> _menu;
rpl::event_stream<> _modifyRequests;
};
} // namespace Ui

View File

@@ -0,0 +1,26 @@
/*
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/rp_widget.h"
namespace Ui {
class AbstractSinglePreview : public RpWidget {
public:
using RpWidget::RpWidget;
[[nodiscard]] virtual rpl::producer<> deleteRequests() const = 0;
[[nodiscard]] virtual rpl::producer<> editRequests() const = 0;
[[nodiscard]] virtual rpl::producer<> modifyRequests() const = 0;
[[nodiscard]] virtual rpl::producer<> editCoverRequests() const = 0;
[[nodiscard]] virtual rpl::producer<> clearCoverRequests() const = 0;
};
} // namespace Ui

View File

@@ -0,0 +1,675 @@
/*
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 "ui/chat/attach/attach_album_preview.h"
#include "ui/chat/attach/attach_album_thumbnail.h"
#include "ui/chat/attach/attach_prepare.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_chat.h"
#include "styles/style_boxes.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
#include <QtWidgets/QApplication>
namespace Media::Streaming {
[[nodiscard]] QImage PrepareBlurredBackground(QSize outer, QImage frame);
} // namespace Media::Streaming
namespace Ui {
namespace {
constexpr auto kDragDuration = crl::time(200);
} // namespace
AlbumPreview::AlbumPreview(
QWidget *parent,
const style::ComposeControls &st,
gsl::span<Ui::PreparedFile> items,
SendFilesWay way,
Fn<bool(int, AttachActionType)> actionAllowed)
: RpWidget(parent)
, _st(st)
, _sendWay(way)
, _actionAllowed(std::move(actionAllowed))
, _dragTimer([=] { switchToDrag(); }) {
setMouseTracking(true);
prepareThumbs(items);
updateSize();
updateFileRows();
}
AlbumPreview::~AlbumPreview() = default;
void AlbumPreview::setSendWay(SendFilesWay way) {
if (_sendWay != way) {
cancelDrag();
_sendWay = way;
}
updateSize();
updateFileRows();
update();
}
void AlbumPreview::updateFileRows() {
Expects(_order.size() == _thumbs.size());
const auto isFile = !_sendWay.sendImagesAsPhotos();
auto top = 0;
for (auto i = 0; i < _order.size(); i++) {
const auto &thumb = _thumbs[_order[i]];
thumb->setButtonVisible(isFile && !thumb->isCompressedSticker());
thumb->moveButtons(top);
top += thumb->fileHeight() + st::sendMediaRowSkip;
}
}
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;
}
bool AlbumPreview::canHaveSpoiler(int index) const {
return _sendWay.sendImagesAsPhotos();
}
void AlbumPreview::toggleSpoilers(bool enabled) {
for (auto &thumb : _thumbs) {
thumb->setSpoiler(enabled);
}
}
std::vector<int> AlbumPreview::takeOrder() {
//Expects(_thumbs.size() == _order.size());
//Expects(_itemsShownDimensions.size() == _order.size());
auto reordered = std::vector<std::unique_ptr<AlbumThumbnail>>();
auto reorderedShownDimensions = std::vector<QSize>();
reordered.reserve(_thumbs.size());
reorderedShownDimensions.reserve(_itemsShownDimensions.size());
for (auto index : _order) {
reordered.push_back(std::move(_thumbs[index]));
reorderedShownDimensions.push_back(_itemsShownDimensions[index]);
}
_thumbs = std::move(reordered);
_itemsShownDimensions = std::move(reorderedShownDimensions);
return std::exchange(_order, defaultOrder());
}
auto AlbumPreview::generateOrderedLayout() const
-> std::vector<GroupMediaLayout> {
auto layout = LayoutMediaGroup(
_itemsShownDimensions,
st::sendMediaPreviewSize,
st::historyGroupWidthMin / 2,
st::historyGroupSkip / 2);
Assert(layout.size() == _order.size());
return layout;
}
std::vector<int> AlbumPreview::defaultOrder(int count) const {
if (count < 0) {
count = _order.size();
}
return ranges::views::ints(0, count) | ranges::to_vector;
}
void AlbumPreview::prepareThumbs(gsl::span<Ui::PreparedFile> items) {
_order = defaultOrder(items.size());
_itemsShownDimensions = ranges::views::all(
_order
) | ranges::views::transform([&](int index) {
return items[index].shownDimensions;
}) | ranges::to_vector;
const auto count = int(_order.size());
const auto layout = generateOrderedLayout();
_thumbs.reserve(count);
for (auto i = 0; i != count; ++i) {
_thumbs.push_back(std::make_unique<AlbumThumbnail>(
_st,
items[i],
layout[i],
this,
[=] { update(); },
[=] { changeThumbByIndex(orderIndex(thumbUnderCursor())); },
[=] { deleteThumbByIndex(orderIndex(thumbUnderCursor())); }));
if (_thumbs.back()->isCompressedSticker()) {
_hasMixedFileHeights = true;
}
}
_thumbsHeight = countLayoutHeight(layout);
_photosHeight = ranges::accumulate(ranges::views::all(
_thumbs
) | ranges::views::transform([](const auto &thumb) {
return thumb->photoHeight();
}), 0) + (count - 1) * st::sendMediaRowSkip;
if (!_hasMixedFileHeights) {
_filesHeight = count * _thumbs.front()->fileHeight()
+ (count - 1) * st::sendMediaRowSkip;
} else {
_filesHeight = ranges::accumulate(ranges::views::all(
_thumbs
) | ranges::views::transform([](const auto &thumb) {
return thumb->fileHeight();
}), 0) + (count - 1) * st::sendMediaRowSkip;
}
}
int AlbumPreview::contentLeft() const {
return (st::boxWideWidth - st::sendMediaPreviewSize) / 2;
}
int AlbumPreview::contentTop() const {
return 0;
}
AlbumThumbnail *AlbumPreview::findThumb(QPoint position) const {
position -= QPoint(contentLeft(), contentTop());
auto top = 0;
const auto isPhotosWay = _sendWay.sendImagesAsPhotos();
const auto skip = st::sendMediaRowSkip;
auto find = [&](const auto &thumb) {
if (_sendWay.groupFiles() && _sendWay.sendImagesAsPhotos()) {
return thumb->containsPoint(position);
} else {
const auto bottom = top + (isPhotosWay
? thumb->photoHeight()
: thumb->fileHeight());
const auto isUnderTop = (position.y() > top);
top = bottom + skip;
return isUnderTop && (position.y() < bottom);
}
return false;
};
const auto i = ranges::find_if(_thumbs, std::move(find));
return (i == _thumbs.end()) ? nullptr : i->get();
}
not_null<AlbumThumbnail*> AlbumPreview::findClosestThumb(
QPoint position) const {
Expects(_draggedThumb != nullptr);
if (const auto exact = findThumb(position)) {
return exact;
}
auto result = _draggedThumb;
auto distance = _draggedThumb->distanceTo(position);
for (const auto &thumb : _thumbs) {
const auto check = thumb->distanceTo(position);
if (check < distance) {
distance = check;
result = thumb.get();
}
}
return result;
}
int AlbumPreview::orderIndex(not_null<AlbumThumbnail*> thumb) const {
const auto i = ranges::find_if(_order, [&](int index) {
return (_thumbs[index].get() == thumb);
});
Assert(i != _order.end());
return int(i - _order.begin());
}
void AlbumPreview::cancelDrag() {
_thumbsHeightAnimation.stop();
_finishDragAnimation.stop();
_shrinkAnimation.stop();
if (_draggedThumb) {
_draggedThumb->moveInAlbum({ 0, 0 });
_draggedThumb = nullptr;
}
if (_suggestedThumb) {
const auto suggestedIndex = orderIndex(_suggestedThumb);
if (suggestedIndex > 0) {
_thumbs[_order[suggestedIndex - 1]]->suggestMove(0., [] {});
}
if (suggestedIndex < int(_order.size() - 1)) {
_thumbs[_order[suggestedIndex + 1]]->suggestMove(0., [] {});
}
_suggestedThumb->suggestMove(0., [] {});
_suggestedThumb->finishAnimations();
_suggestedThumb = nullptr;
}
_paintedAbove = nullptr;
update();
}
void AlbumPreview::finishDrag() {
Expects(_draggedThumb != nullptr);
Expects(_suggestedThumb != nullptr);
if (_suggestedThumb != _draggedThumb) {
const auto currentIndex = orderIndex(_draggedThumb);
const auto newIndex = orderIndex(_suggestedThumb);
const auto delta = (currentIndex < newIndex) ? 1 : -1;
const auto realIndex = _order[currentIndex];
for (auto i = currentIndex; i != newIndex; i += delta) {
_order[i] = _order[i + delta];
}
_order[newIndex] = realIndex;
const auto layout = generateOrderedLayout();
for (auto i = 0, count = int(_order.size()); i != count; ++i) {
_thumbs[_order[i]]->moveToLayout(layout[i]);
}
_finishDragAnimation.start([=] { update(); }, 0., 1., kDragDuration);
updateSizeAnimated(layout);
_orderUpdated.fire({});
} else {
for (const auto &thumb : _thumbs) {
thumb->resetLayoutAnimation();
}
_draggedThumb->animateLayoutToInitial();
_finishDragAnimation.start([=] { update(); }, 0., 1., kDragDuration);
}
}
int AlbumPreview::countLayoutHeight(
const std::vector<GroupMediaLayout> &layout) const {
const auto accumulator = [](int current, const auto &item) {
return std::max(current, item.geometry.y() + item.geometry.height());
};
return ranges::accumulate(layout, 0, accumulator);
}
void AlbumPreview::updateSizeAnimated(
const std::vector<GroupMediaLayout> &layout) {
const auto newHeight = countLayoutHeight(layout);
if (newHeight != _thumbsHeight) {
_thumbsHeightAnimation.start(
[=] { updateSize(); },
_thumbsHeight,
newHeight,
kDragDuration);
_thumbsHeight = newHeight;
}
}
void AlbumPreview::updateSize() {
const auto newHeight = [&] {
if (!_sendWay.sendImagesAsPhotos()) {
return _filesHeight;
} else if (!_sendWay.groupFiles()) {
return _photosHeight;
} else {
return int(base::SafeRound(_thumbsHeightAnimation.value(
_thumbsHeight)));
}
}();
if (height() != newHeight) {
resize(st::boxWideWidth, newHeight);
}
}
void AlbumPreview::paintEvent(QPaintEvent *e) {
Painter p(this);
if (!_sendWay.sendImagesAsPhotos()) {
paintFiles(p, e->rect());
} else if (!_sendWay.groupFiles()) {
paintPhotos(p, e->rect());
} else {
paintAlbum(p);
}
}
void AlbumPreview::paintAlbum(Painter &p) const {
const auto shrink = _shrinkAnimation.value(_draggedThumb ? 1. : 0.);
const auto moveProgress = _finishDragAnimation.value(1.);
const auto left = contentLeft();
const auto top = contentTop();
for (const auto &thumb : _thumbs) {
if (thumb.get() != _paintedAbove) {
thumb->paintInAlbum(p, left, top, shrink, moveProgress);
}
}
if (_paintedAbove) {
_paintedAbove->paintInAlbum(p, left, top, shrink, moveProgress);
}
}
void AlbumPreview::paintPhotos(Painter &p, QRect clip) const {
const auto left = (st::boxWideWidth - st::sendMediaPreviewSize) / 2;
auto top = 0;
const auto outerWidth = width();
for (const auto &thumb : _thumbs) {
const auto bottom = top + thumb->photoHeight();
const auto guard = gsl::finally([&] {
top = bottom + st::sendMediaRowSkip;
});
if (top >= clip.y() + clip.height()) {
break;
} else if (bottom <= clip.y()) {
continue;
}
thumb->paintPhoto(p, left, top, outerWidth);
}
}
void AlbumPreview::paintFiles(Painter &p, QRect clip) const {
const auto left = (st::boxWideWidth - st::sendMediaPreviewSize) / 2;
const auto outerWidth = width();
if (!_hasMixedFileHeights) {
const auto fileHeight = st::attachPreviewThumbLayout.thumbSize
+ st::sendMediaRowSkip;
const auto bottom = clip.y() + clip.height();
const auto from = std::clamp(
clip.y() / fileHeight,
0,
int(_thumbs.size()));
const auto till = std::clamp(
(bottom + fileHeight - 1) / fileHeight,
0,
int(_thumbs.size()));
auto top = from * fileHeight;
for (auto i = from; i != till; ++i) {
_thumbs[i]->paintFile(p, left, top, outerWidth);
top += fileHeight;
}
} else {
auto top = 0;
for (const auto &thumb : _thumbs) {
const auto bottom = top + thumb->fileHeight();
const auto guard = gsl::finally([&] {
top = bottom + st::sendMediaRowSkip;
});
if (top >= clip.y() + clip.height()) {
break;
} else if (bottom <= clip.y()) {
continue;
}
thumb->paintFile(p, left, top, outerWidth);
}
}
}
AlbumThumbnail *AlbumPreview::thumbUnderCursor() {
return findThumb(mapFromGlobal(QCursor::pos()));
}
void AlbumPreview::deleteThumbByIndex(int index) {
if (index < 0) {
return;
}
_thumbDeleted.fire(std::move(index));
}
void AlbumPreview::changeThumbByIndex(int index) {
if (index < 0) {
return;
}
_thumbChanged.fire(std::move(index));
}
void AlbumPreview::modifyThumbByIndex(int index) {
if (index < 0) {
return;
}
_thumbModified.fire(std::move(index));
}
void AlbumPreview::thumbButtonsCallback(
not_null<AlbumThumbnail*> thumb,
AttachButtonType type) {
const auto index = orderIndex(thumb);
switch (type) {
case AttachButtonType::None: return;
case AttachButtonType::Edit: changeThumbByIndex(index); break;
case AttachButtonType::Delete: deleteThumbByIndex(index); break;
case AttachButtonType::Modify:
cancelDrag();
modifyThumbByIndex(index);
break;
}
}
void AlbumPreview::mousePressEvent(QMouseEvent *e) {
if (_finishDragAnimation.animating()) {
return;
}
const auto position = e->pos();
cancelDrag();
if (const auto thumb = findThumb(position)) {
_draggedStartPosition = position;
_pressedThumb = thumb;
_pressedButtonType = thumb->buttonTypeFromPoint(position);
const auto isAlbum = _sendWay.sendImagesAsPhotos()
&& _sendWay.groupFiles();
if (!isAlbum || e->button() != Qt::LeftButton) {
_dragTimer.cancel();
return;
}
if (_pressedButtonType == AttachButtonType::None) {
switchToDrag();
} else if (_pressedButtonType == AttachButtonType::Modify) {
_dragTimer.callOnce(QApplication::startDragTime());
}
}
}
void AlbumPreview::mouseMoveEvent(QMouseEvent *e) {
if (!_sendWay.sendImagesAsPhotos() && !_hasMixedFileHeights) {
applyCursor(style::cur_default);
return;
}
if (_dragTimer.isActive()) {
_dragTimer.cancel();
switchToDrag();
}
const auto isAlbum = _sendWay.sendImagesAsPhotos()
&& _sendWay.groupFiles();
if (isAlbum && _draggedThumb) {
const auto position = e->pos();
_draggedThumb->moveInAlbum(position - _draggedStartPosition);
updateSuggestedDrag(_draggedThumb->center());
update();
} else {
const auto thumb = findThumb(e->pos());
const auto regularCursor = isAlbum
? style::cur_pointer
: style::cur_default;
const auto cursor = thumb
? (thumb->buttonsContainPoint(e->pos())
? style::cur_pointer
: regularCursor)
: style::cur_default;
applyCursor(cursor);
}
}
void AlbumPreview::applyCursor(style::cursor cursor) {
if (_cursor != cursor) {
_cursor = cursor;
setCursor(_cursor);
}
}
void AlbumPreview::updateSuggestedDrag(QPoint position) {
auto closest = findClosestThumb(position);
auto closestIndex = orderIndex(closest);
const auto draggedIndex = orderIndex(_draggedThumb);
const auto closestIsBeforePoint = closest->isPointAfter(position);
if (closestIndex < draggedIndex && closestIsBeforePoint) {
closest = _thumbs[_order[++closestIndex]].get();
} else if (closestIndex > draggedIndex && !closestIsBeforePoint) {
closest = _thumbs[_order[--closestIndex]].get();
}
if (_suggestedThumb == closest) {
return;
}
const auto last = int(_order.size()) - 1;
if (_suggestedThumb) {
const auto suggestedIndex = orderIndex(_suggestedThumb);
if (suggestedIndex < draggedIndex && suggestedIndex > 0) {
const auto previous = _thumbs[_order[suggestedIndex - 1]].get();
previous->suggestMove(0., [=] { update(); });
} else if (suggestedIndex > draggedIndex && suggestedIndex < last) {
const auto next = _thumbs[_order[suggestedIndex + 1]].get();
next->suggestMove(0., [=] { update(); });
}
_suggestedThumb->suggestMove(0., [=] { update(); });
}
_suggestedThumb = closest;
const auto suggestedIndex = closestIndex;
if (_suggestedThumb != _draggedThumb) {
const auto delta = (suggestedIndex < draggedIndex) ? 1. : -1.;
if (delta > 0. && suggestedIndex > 0) {
const auto previous = _thumbs[_order[suggestedIndex - 1]].get();
previous->suggestMove(-delta, [=] { update(); });
} else if (delta < 0. && suggestedIndex < last) {
const auto next = _thumbs[_order[suggestedIndex + 1]].get();
next->suggestMove(-delta, [=] { update(); });
}
_suggestedThumb->suggestMove(delta, [=] { update(); });
}
}
void AlbumPreview::mouseReleaseEvent(QMouseEvent *e) {
if (_draggedThumb) {
finishDrag();
_shrinkAnimation.start(
[=] { update(); },
1.,
0.,
AlbumThumbnail::kShrinkDuration);
_draggedThumb = nullptr;
_suggestedThumb = nullptr;
update();
} else if (const auto thumb = base::take(_pressedThumb)) {
const auto was = _pressedButtonType;
const auto now = thumb->buttonTypeFromPoint(e->pos());
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) {
_menu = base::make_unique_q<Ui::PopupMenu>(
this,
st::popupMenuWithIcons);
const auto index = orderIndex(thumb);
if (_actionAllowed(index, AttachActionType::ToggleSpoiler)
&& _sendWay.sendImagesAsPhotos()) {
const auto spoilered = thumb->hasSpoiler();
_menu->addAction(spoilered
? tr::lng_context_disable_spoiler(tr::now)
: tr::lng_context_spoiler_effect(tr::now), [=] {
thumb->setSpoiler(!spoilered);
}, spoilered ? &st::menuIconSpoilerOff : &st::menuIconSpoiler);
}
if (_actionAllowed(index, AttachActionType::EditCover)) {
_menu->addAction(tr::lng_context_edit_cover(tr::now), [=] {
_thumbEditCoverRequested.fire_copy(index);
}, &st::menuIconEdit);
if (_actionAllowed(index, AttachActionType::ClearCover)) {
_menu->addAction(tr::lng_context_clear_cover(tr::now), [=] {
_thumbClearCoverRequested.fire_copy(index);
}, &st::menuIconCancel);
}
}
if (_menu->empty()) {
_menu = nullptr;
} else {
_menu->popup(position);
}
}
void AlbumPreview::switchToDrag() {
_paintedAbove
= _suggestedThumb
= _draggedThumb
= base::take(_pressedThumb);
_shrinkAnimation.start(
[=] { update(); },
0.,
1.,
AlbumThumbnail::kShrinkDuration);
applyCursor(style::cur_sizeall);
update();
}
QImage AlbumPreview::generatePriceTagBackground() const {
auto wmax = 0;
auto hmax = 0;
for (auto &thumb : _thumbs) {
const auto geometry = thumb->geometry();
accumulate_max(wmax, geometry.x() + geometry.width());
accumulate_max(hmax, geometry.y() + geometry.height());
}
const auto size = QSize(wmax, hmax);
if (size.isEmpty()) {
return {};
}
const auto ratio = style::DevicePixelRatio();
const auto full = size * ratio;
const auto skip = st::historyGroupSkip;
auto result = QImage(full, QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(ratio);
result.fill(Qt::black);
auto p = QPainter(&result);
auto hq = PainterHighQualityEnabler(p);
for (auto &thumb : _thumbs) {
const auto geometry = thumb->geometry();
if (geometry.isEmpty()) {
continue;
}
const auto w = geometry.width();
const auto h = geometry.height();
const auto wscale = (w + skip) / float64(w);
const auto hscale = (h + skip) / float64(h);
p.save();
p.translate(geometry.center());
p.scale(wscale, hscale);
p.translate(-geometry.center());
thumb->paintInAlbum(p, 0, 0, 1., 1.);
p.restore();
}
p.end();
return ::Media::Streaming::PrepareBlurredBackground(
full,
std::move(result));
}
} // namespace Ui

View File

@@ -0,0 +1,142 @@
/*
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/rp_widget.h"
#include "ui/chat/attach/attach_send_files_way.h"
#include "base/timer.h"
namespace style {
struct ComposeControls;
} // namespace style
namespace Ui {
struct PreparedFile;
struct GroupMediaLayout;
class AlbumThumbnail;
class PopupMenu;
class AlbumPreview final : public RpWidget {
public:
AlbumPreview(
QWidget *parent,
const style::ComposeControls &st,
gsl::span<Ui::PreparedFile> items,
SendFilesWay way,
Fn<bool(int, AttachActionType)> actionAllowed);
~AlbumPreview();
void setSendWay(SendFilesWay way);
[[nodiscard]] base::flat_set<int> collectSpoileredIndices();
[[nodiscard]] bool canHaveSpoiler(int index) const;
void toggleSpoilers(bool enabled);
[[nodiscard]] std::vector<int> takeOrder();
[[nodiscard]] rpl::producer<int> thumbDeleted() const {
return _thumbDeleted.events();
}
[[nodiscard]] rpl::producer<int> thumbChanged() const {
return _thumbChanged.events();
}
[[nodiscard]] rpl::producer<int> thumbModified() const {
return _thumbModified.events();
}
[[nodiscard]] rpl::producer<int> thumbEditCoverRequested() const {
return _thumbEditCoverRequested.events();
}
[[nodiscard]] rpl::producer<int> thumbClearCoverRequested() const {
return _thumbClearCoverRequested.events();
}
[[nodiscard]] rpl::producer<> orderUpdated() const {
return _orderUpdated.events();
}
[[nodiscard]] QImage generatePriceTagBackground() const;
protected:
void paintEvent(QPaintEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
private:
int countLayoutHeight(
const std::vector<GroupMediaLayout> &layout) const;
std::vector<GroupMediaLayout> generateOrderedLayout() const;
std::vector<int> defaultOrder(int count = -1) const;
void prepareThumbs(gsl::span<Ui::PreparedFile> items);
void updateSizeAnimated(const std::vector<GroupMediaLayout> &layout);
void updateSize();
void updateFileRows();
AlbumThumbnail *thumbUnderCursor();
void deleteThumbByIndex(int index);
void changeThumbByIndex(int index);
void modifyThumbByIndex(int index);
void thumbButtonsCallback(
not_null<AlbumThumbnail*> thumb,
AttachButtonType type);
void switchToDrag();
void paintAlbum(Painter &p) const;
void paintPhotos(Painter &p, QRect clip) const;
void paintFiles(Painter &p, QRect clip) const;
void applyCursor(style::cursor cursor);
int contentLeft() const;
int contentTop() const;
AlbumThumbnail *findThumb(QPoint position) const;
not_null<AlbumThumbnail*> findClosestThumb(QPoint position) const;
void updateSuggestedDrag(QPoint position);
int orderIndex(not_null<AlbumThumbnail*> thumb) const;
void cancelDrag();
void finishDrag();
void showContextMenu(not_null<AlbumThumbnail*> thumb, QPoint position);
const style::ComposeControls &_st;
SendFilesWay _sendWay;
Fn<bool(int, AttachActionType)> _actionAllowed;
style::cursor _cursor = style::cur_default;
std::vector<int> _order;
std::vector<QSize> _itemsShownDimensions;
std::vector<std::unique_ptr<AlbumThumbnail>> _thumbs;
int _thumbsHeight = 0;
int _photosHeight = 0;
int _filesHeight = 0;
bool _hasMixedFileHeights = false;
AlbumThumbnail *_draggedThumb = nullptr;
AlbumThumbnail *_suggestedThumb = nullptr;
AlbumThumbnail *_paintedAbove = nullptr;
AlbumThumbnail *_pressedThumb = nullptr;
QPoint _draggedStartPosition;
base::Timer _dragTimer;
AttachButtonType _pressedButtonType = AttachButtonType::None;
rpl::event_stream<int> _thumbDeleted;
rpl::event_stream<int> _thumbChanged;
rpl::event_stream<int> _thumbModified;
rpl::event_stream<int> _thumbEditCoverRequested;
rpl::event_stream<int> _thumbClearCoverRequested;
rpl::event_stream<> _orderUpdated;
base::unique_qptr<PopupMenu> _menu;
mutable Animations::Simple _thumbsHeightAnimation;
mutable Animations::Simple _shrinkAnimation;
mutable Animations::Simple _finishDragAnimation;
};
} // namespace Ui

View File

@@ -0,0 +1,626 @@
/*
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 "ui/chat/attach/attach_album_thumbnail.h"
#include "core/mime_type.h" // Core::IsMimeSticker.
#include "ui/chat/attach/attach_prepare.h"
#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 "ui/power_saving.h"
#include "base/call_delayed.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_boxes.h"
#include <QtCore/QFileInfo>
namespace Ui {
AlbumThumbnail::AlbumThumbnail(
const style::ComposeControls &st,
const PreparedFile &file,
const GroupMediaLayout &layout,
QWidget *parent,
Fn<void()> repaint,
Fn<void()> editCallback,
Fn<void()> deleteCallback)
: _st(st)
, _layout(layout)
, _fullPreview(file.videoCover ? file.videoCover->preview : file.preview)
, _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))
, _repaint(std::move(repaint)) {
Expects(!_fullPreview.isNull());
moveToLayout(layout);
using Option = Images::Option;
const auto previewWidth = _fullPreview.width();
const auto previewHeight = _fullPreview.height();
const auto imageWidth = std::max(
previewWidth / style::DevicePixelRatio(),
st::minPhotoSize);
const auto imageHeight = std::max(
previewHeight / style::DevicePixelRatio(),
st::minPhotoSize);
_photo = PixmapFromImage(Images::Prepare(
_fullPreview,
QSize(previewWidth, previewHeight),
{
.options = Option::RoundLarge,
.outer = { imageWidth, imageHeight },
}));
const auto &layoutSt = st::attachPreviewThumbLayout;
const auto idealSize = layoutSt.thumbSize * style::DevicePixelRatio();
const auto fileThumbSize = (previewWidth > previewHeight)
? QSize(previewWidth * idealSize / previewHeight, idealSize)
: QSize(idealSize, previewHeight * idealSize / previewWidth);
_fileThumb = PixmapFromImage(Images::Prepare(
_fullPreview,
fileThumbSize,
{
.options = Option::RoundSmall,
.outer = { layoutSt.thumbSize, layoutSt.thumbSize },
}));
const auto availableFileWidth = st::sendMediaPreviewSize
- layoutSt.thumbSize
- layoutSt.thumbSkip
// Right buttons.
- st::sendBoxAlbumGroupButtonFile.width * 2
- st::sendBoxAlbumGroupEditInternalSkip * 2
- st::sendBoxAlbumGroupSkipRight;
const auto filepath = file.path;
if (filepath.isEmpty()) {
_name = "image.png";
_status = FormatImageSizeText(file.originalDimensions);
} else {
auto fileinfo = QFileInfo(filepath);
_name = fileinfo.fileName();
_status = FormatSizeText(fileinfo.size());
}
_nameWidth = st::semiboldFont->width(_name);
if (_nameWidth > availableFileWidth) {
_name = st::semiboldFont->elided(
_name,
availableFileWidth,
Qt::ElideMiddle);
_nameWidth = st::semiboldFont->width(_name);
}
_statusWidth = st::normalFont->width(_status);
_editMedia.create(parent, _st.files.buttonFile);
_deleteMedia.create(parent, _st.files.buttonFile);
const auto duration = st::historyAttach.ripple.hideDuration;
_editMedia->setClickedCallback([=] {
base::call_delayed(duration, parent, editCallback);
});
_deleteMedia->setClickedCallback(deleteCallback);
_editMedia->setIconOverride(&_st.files.buttonFileEdit);
_deleteMedia->setIconOverride(&_st.files.buttonFileDelete);
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);
}
void AlbumThumbnail::moveButtons(int thumbTop) {
const auto top = thumbTop + st::sendBoxFileGroupSkipTop;
auto right = st::sendBoxFileGroupSkipRight + st::boxPhotoPadding.right();
_deleteMedia->moveToRight(right, top);
right += st::sendBoxFileGroupEditInternalSkip + _deleteMedia->width();
_editMedia->moveToRight(right, top);
}
void AlbumThumbnail::resetLayoutAnimation() {
_animateFromGeometry = std::nullopt;
}
void AlbumThumbnail::animateLayoutToInitial() {
_animateFromGeometry = countRealGeometry();
_suggestedMove = 0.;
_albumPosition = QPoint(0, 0);
}
void AlbumThumbnail::moveToLayout(const GroupMediaLayout &layout) {
using namespace Images;
animateLayoutToInitial();
_layout = layout;
const auto width = _layout.geometry.width();
const auto height = _layout.geometry.height();
_albumCorners = GetCornersFromSides(_layout.sides);
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(Prepare(
_fullPreview,
QSize(pixWidth, pixHeight),
{
.options = RoundOptions(ImageRoundRadius::Large, _albumCorners),
.outer = { width, height },
}));
_albumImageBlurred = QPixmap();
}
int AlbumThumbnail::photoHeight() const {
return _photo.height() / style::DevicePixelRatio();
}
int AlbumThumbnail::fileHeight() const {
return _isCompressedSticker
? photoHeight()
: st::attachPreviewThumbLayout.thumbSize;
}
bool AlbumThumbnail::isCompressedSticker() const {
return _isCompressedSticker;
}
void AlbumThumbnail::paintInAlbum(
QPainter &p,
int left,
int top,
float64 shrinkProgress,
float64 moveProgress) {
const auto shrink = anim::interpolate(0, _shrinkSize, shrinkProgress);
_lastShrinkValue = shrink;
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 }
);
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);
}
}
if (revealed < 1.) {
auto corners = Images::CornersMaskRef(
Images::CornersMask(ImageRoundRadius::Large));
if (!(_albumCorners & RectPart::TopLeft)) {
corners.p[0] = nullptr;
}
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);
const auto paused = On(PowerSaving::kChatSpoiler);
FillSpoilerRect(
p,
paintedTo,
corners,
DefaultImageSpoiler().frame(_spoiler->index(crl::now(), paused)),
_cornerCache);
p.setOpacity(1.);
}
_lastRectOfButtons = paintButtons(
p,
geometry,
shrinkProgress);
_lastRectOfModify = geometry;
}
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) {
const auto width = std::max(
_layout.geometry.width(),
_animateFromGeometry ? _animateFromGeometry->width() : 0);
const auto height = std::max(
_layout.geometry.height(),
_animateFromGeometry ? _animateFromGeometry->height() : 0);
const auto cacheSize = QSize(width, height) * style::DevicePixelRatio();
if (_albumCache.width() < cacheSize.width()
|| _albumCache.height() < cacheSize.height()) {
_albumCache = QImage(cacheSize, QImage::Format_ARGB32_Premultiplied);
_albumCache.setDevicePixelRatio(style::DevicePixelRatio());
}
_albumCache.fill(Qt::transparent);
{
Painter p(&_albumCache);
const auto to = QRect(QPoint(), size).marginsRemoved(
{ shrink, shrink, shrink, shrink }
);
drawSimpleFrame(p, to, size);
}
_albumCache = Images::Round(
std::move(_albumCache),
ImageRoundRadius::Large,
_albumCorners,
QRect(QPoint(), size * style::DevicePixelRatio()));
}
void AlbumThumbnail::drawSimpleFrame(QPainter &p, QRect to, QSize size) const {
const auto fullWidth = _fullPreview.width();
const auto fullHeight = _fullPreview.height();
const auto previewSize = GetImageScaleSizeForGeometry(
{ fullWidth, fullHeight },
{ size.width(), size.height() });
const auto previewWidth = previewSize.width() * style::DevicePixelRatio();
const auto previewHeight = previewSize.height() * style::DevicePixelRatio();
const auto width = size.width() * style::DevicePixelRatio();
const auto height = size.height() * style::DevicePixelRatio();
const auto scaleWidth = to.width() / float64(width);
const auto scaleHeight = to.height() / float64(height);
const auto Round = [](float64 value) {
return int(base::SafeRound(value));
};
const auto &[from, fillBlack] = [&] {
if (previewWidth < width && previewHeight < height) {
const auto toWidth = Round(previewWidth * scaleWidth);
const auto toHeight = Round(previewHeight * scaleHeight);
return std::make_pair(
QRect(0, 0, fullWidth, fullHeight),
QMargins(
(to.width() - toWidth) / 2,
(to.height() - toHeight) / 2,
to.width() - toWidth - (to.width() - toWidth) / 2,
to.height() - toHeight - (to.height() - toHeight) / 2));
} else if (previewWidth * height > previewHeight * width) {
if (previewHeight >= height) {
const auto takeWidth = previewWidth * height / previewHeight;
const auto useWidth = fullWidth * width / takeWidth;
return std::make_pair(
QRect(
(fullWidth - useWidth) / 2,
0,
useWidth,
fullHeight),
QMargins(0, 0, 0, 0));
} else {
const auto takeWidth = previewWidth;
const auto useWidth = fullWidth * width / takeWidth;
const auto toHeight = Round(previewHeight * scaleHeight);
const auto toSkip = (to.height() - toHeight) / 2;
return std::make_pair(
QRect(
(fullWidth - useWidth) / 2,
0,
useWidth,
fullHeight),
QMargins(
0,
toSkip,
0,
to.height() - toHeight - toSkip));
}
} else {
if (previewWidth >= width) {
const auto takeHeight = previewHeight * width / previewWidth;
const auto useHeight = fullHeight * height / takeHeight;
return std::make_pair(
QRect(
0,
(fullHeight - useHeight) / 2,
fullWidth,
useHeight),
QMargins(0, 0, 0, 0));
} else {
const auto takeHeight = previewHeight;
const auto useHeight = fullHeight * height / takeHeight;
const auto toWidth = Round(previewWidth * scaleWidth);
const auto toSkip = (to.width() - toWidth) / 2;
return std::make_pair(
QRect(
0,
(fullHeight - useHeight) / 2,
fullWidth,
useHeight),
QMargins(
toSkip,
0,
to.width() - toWidth - toSkip,
0));
}
}
}();
p.drawImage(to.marginsRemoved(fillBlack), _fullPreview, from);
if (fillBlack.top() > 0) {
p.fillRect(to.x(), to.y(), to.width(), fillBlack.top(), st::imageBg);
}
if (fillBlack.bottom() > 0) {
p.fillRect(
to.x(),
to.y() + to.height() - fillBlack.bottom(),
to.width(),
fillBlack.bottom(),
st::imageBg);
}
if (fillBlack.left() > 0) {
p.fillRect(
to.x(),
to.y() + fillBlack.top(),
fillBlack.left(),
to.height() - fillBlack.top() - fillBlack.bottom(),
st::imageBg);
}
if (fillBlack.right() > 0) {
p.fillRect(
to.x() + to.width() - fillBlack.right(),
to.y() + fillBlack.top(),
fillBlack.right(),
to.height() - fillBlack.top() - fillBlack.bottom(),
st::imageBg);
}
}
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,
pixmap);
if (_spoiler) {
const auto paused = On(PowerSaving::kChatSpoiler);
FillSpoilerRect(
p,
rect,
Images::CornersMaskRef(
Images::CornersMask(ImageRoundRadius::Large)),
DefaultImageSpoiler().frame(_spoiler->index(crl::now(), paused)),
_cornerCache);
} else if (_isVideo) {
paintPlayVideo(p, rect);
}
const auto topLeft = QPoint{ left, top };
_lastRectOfButtons = paintButtons(
p,
QRect(left, top, st::sendMediaPreviewSize, size.height()),
0);
_lastRectOfModify = QRect(topLeft, size);
}
void AlbumThumbnail::paintFile(
Painter &p,
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;
p.drawPixmap(left, top, _fileThumb);
p.setFont(st::semiboldFont);
p.setPen(_st.files.nameFg);
p.drawTextLeft(
textLeft,
top + st.nameTop,
outerWidth,
_name,
_nameWidth);
p.setFont(st::normalFont);
p.setPen(_st.files.statusFg);
p.drawTextLeft(
textLeft,
top + st.statusTop,
outerWidth,
_status,
_statusWidth);
_lastRectOfModify = QRect(
QPoint(left, top),
_fileThumb.size() / style::DevicePixelRatio());
}
QRect AlbumThumbnail::geometry() const {
return _layout.geometry;
}
bool AlbumThumbnail::containsPoint(QPoint position) const {
return _layout.geometry.contains(position);
}
bool AlbumThumbnail::buttonsContainPoint(QPoint position) const {
return ((_isPhoto && !_isCompressedSticker)
? _lastRectOfModify
: _lastRectOfButtons).contains(position);
}
AttachButtonType AlbumThumbnail::buttonTypeFromPoint(QPoint position) const {
if (!buttonsContainPoint(position)) {
return AttachButtonType::None;
}
return (!_lastRectOfButtons.contains(position) && !_isCompressedSticker)
? AttachButtonType::Modify
: (_buttons.vertical()
? (position.y() < _lastRectOfButtons.center().y())
: (position.x() < _lastRectOfButtons.center().x()))
? AttachButtonType::Edit
: AttachButtonType::Delete;
}
int AlbumThumbnail::distanceTo(QPoint position) const {
const auto delta = (_layout.geometry.center() - position);
return QPoint::dotProduct(delta, delta);
}
bool AlbumThumbnail::isPointAfter(QPoint position) const {
return position.x() > _layout.geometry.center().x();
}
void AlbumThumbnail::moveInAlbum(QPoint to) {
_albumPosition = to;
}
QPoint AlbumThumbnail::center() const {
auto realGeometry = _layout.geometry;
realGeometry.moveTopLeft(realGeometry.topLeft() + _albumPosition);
return realGeometry.center();
}
void AlbumThumbnail::suggestMove(float64 delta, Fn<void()> callback) {
if (_suggestedMove != delta) {
_suggestedMoveAnimation.start(
std::move(callback),
_suggestedMove,
delta,
kShrinkDuration);
_suggestedMove = delta;
}
}
QRect AlbumThumbnail::countRealGeometry() const {
const auto addLeft = int(base::SafeRound(
_suggestedMoveAnimation.value(_suggestedMove) * _lastShrinkValue));
const auto current = _layout.geometry;
const auto realTopLeft = current.topLeft()
+ _albumPosition
+ QPoint(addLeft, 0);
return { realTopLeft, current.size() };
}
QRect AlbumThumbnail::countCurrentGeometry(float64 progress) const {
const auto now = countRealGeometry();
if (_animateFromGeometry && progress < 1.) {
return {
anim::interpolate(_animateFromGeometry->x(), now.x(), progress),
anim::interpolate(_animateFromGeometry->y(), now.y(), progress),
anim::interpolate(_animateFromGeometry->width(), now.width(), progress),
anim::interpolate(_animateFromGeometry->height(), now.height(), progress)
};
}
return now;
}
void AlbumThumbnail::finishAnimations() {
_suggestedMoveAnimation.stop();
}
QRect AlbumThumbnail::paintButtons(
QPainter &p,
QRect geometry,
float64 shrinkProgress) {
const auto &skipRight = st::sendBoxAlbumGroupSkipRight;
const auto &skipTop = st::sendBoxAlbumGroupSkipTop;
const auto outerWidth = geometry.width();
const auto outerHeight = geometry.height();
if (st::sendBoxAlbumGroupSize.width() <= outerWidth) {
_buttons.setVertical(false);
} else if (st::sendBoxAlbumGroupSize.height() <= outerHeight) {
_buttons.setVertical(true);
} else {
// If the size is tiny, skip the buttons.
return QRect();
}
const auto groupWidth = _buttons.width();
const auto groupHeight = _buttons.height();
// If the width is too small,
// it would be better to display the buttons in the center.
const auto groupX = geometry.x() + ((groupWidth + skipRight * 2 > outerWidth)
? (outerWidth - groupWidth) / 2
: outerWidth - skipRight - groupWidth);
const auto groupY = geometry.y() + ((groupHeight + skipTop * 2 > outerHeight)
? (outerHeight - groupHeight) / 2
: skipTop);
const auto opacity = p.opacity();
p.setOpacity(1.0 - shrinkProgress);
_buttons.paint(p, groupX, groupY);
p.setOpacity(opacity);
return QRect(groupX, groupY, groupWidth, _buttons.height());
}
} // namespace Ui

View File

@@ -0,0 +1,124 @@
/*
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/attach/attach_controls.h"
#include "ui/chat/attach/attach_send_files_way.h"
#include "ui/effects/animations.h"
#include "ui/grouped_layout.h"
#include "ui/round_rect.h"
#include "base/object_ptr.h"
namespace style {
struct ComposeControls;
} // namespace style
namespace Ui {
struct PreparedFile;
class IconButton;
class SpoilerAnimation;
class AlbumThumbnail final {
public:
AlbumThumbnail(
const style::ComposeControls &st,
const PreparedFile &file,
const GroupMediaLayout &layout,
QWidget *parent,
Fn<void()> repaint,
Fn<void()> editCallback,
Fn<void()> deleteCallback);
void moveToLayout(const GroupMediaLayout &layout);
void animateLayoutToInitial();
void resetLayoutAnimation();
void setSpoiler(bool spoiler);
[[nodiscard]] bool hasSpoiler() const;
[[nodiscard]] int photoHeight() const;
[[nodiscard]] int fileHeight() const;
void paintInAlbum(
QPainter &p,
int left,
int top,
float64 shrinkProgress,
float64 moveProgress);
void paintPhoto(Painter &p, int left, int top, int outerWidth);
void paintFile(Painter &p, int left, int top, int outerWidth);
[[nodiscard]] QRect geometry() const;
[[nodiscard]] bool containsPoint(QPoint position) const;
[[nodiscard]] bool buttonsContainPoint(QPoint position) const;
[[nodiscard]] AttachButtonType buttonTypeFromPoint(
QPoint position) const;
[[nodiscard]] int distanceTo(QPoint position) const;
[[nodiscard]] bool isPointAfter(QPoint position) const;
void moveInAlbum(QPoint to);
[[nodiscard]] QPoint center() const;
void suggestMove(float64 delta, Fn<void()> callback);
void finishAnimations();
void setButtonVisible(bool value);
void moveButtons(int thumbTop);
[[nodiscard]] bool isCompressedSticker() const;
static constexpr auto kShrinkDuration = crl::time(150);
private:
QRect countRealGeometry() const;
QRect countCurrentGeometry(float64 progress) const;
void prepareCache(QSize size, int shrink);
void drawSimpleFrame(QPainter &p, QRect to, QSize size) const;
QRect paintButtons(
QPainter &p,
QRect geometry,
float64 shrinkProgress);
void paintPlayVideo(QPainter &p, QRect geometry);
const style::ComposeControls &_st;
GroupMediaLayout _layout;
std::optional<QRect> _animateFromGeometry;
const QImage _fullPreview;
const int _shrinkSize;
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;
int _nameWidth = 0;
int _statusWidth = 0;
float64 _suggestedMove = 0.;
Animations::Simple _suggestedMoveAnimation;
int _lastShrinkValue = 0;
AttachControls _buttons;
bool _isCompressedSticker = false;
std::unique_ptr<SpoilerAnimation> _spoiler;
QImage _cornerCache;
Fn<void()> _repaint;
QRect _lastRectOfModify;
QRect _lastRectOfButtons;
object_ptr<IconButton> _editMedia = { nullptr };
object_ptr<IconButton> _deleteMedia = { nullptr };
};
} // namespace Ui

View File

@@ -0,0 +1,268 @@
/*
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 "ui/chat/attach/attach_bot_downloads.h"
#include "lang/lang_keys.h"
#include "ui/widgets/menu/menu_item_base.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/popup_menu.h"
#include "ui/effects/ripple_animation.h"
#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "ui/painter.h"
#include "styles/style_chat.h"
namespace Ui::BotWebView {
namespace {
class Action final : public Menu::ItemBase {
public:
Action(
not_null<RpWidget*> parent,
const DownloadsEntry &entry,
Fn<void(DownloadsAction)> callback);
bool isEnabled() const override;
not_null<QAction*> action() const override { return _dummyAction; }
void handleKeyPress(not_null<QKeyEvent*> e) override;
void refresh(const DownloadsEntry &entry);
private:
QPoint prepareRippleStartPosition() const override {
return mapFromGlobal(QCursor::pos());
}
QImage prepareRippleMask() const override {
return Ui::RippleAnimation::RectMask(size());
}
int contentHeight() const override { return _height; }
void prepare();
void paint(Painter &p);
const not_null<QAction*> _dummyAction;
const style::Menu &_st = st::defaultMenu;
DownloadsEntry _entry;
Text::String _name;
FlatLabel _progress;
IconButton _cancel;
int _textWidth = 0;
const int _height;
};
Action::Action(
not_null<RpWidget*> parent,
const DownloadsEntry &entry,
Fn<void(DownloadsAction)> callback)
: ItemBase(parent, st::defaultMenu)
, _dummyAction(new QAction(parent))
, _progress(this, st::botDownloadProgress)
, _cancel(this, st::botDownloadCancel)
, _height(st::ttlItemPadding.top()
+ _st.itemStyle.font->height
+ st::ttlItemTimerFont->height
+ st::ttlItemPadding.bottom()) {
setAcceptBoth(true);
initResizeHook(parent->sizeValue());
setClickedCallback([=] {
if (isEnabled()) {
callback(DownloadsAction::Open);
}
});
_cancel.setClickedCallback([=] {
callback(DownloadsAction::Cancel);
});
paintRequest(
) | rpl::on_next([=] {
Painter p(this);
paint(p);
}, lifetime());
widthValue() | rpl::on_next([=](int width) {
_progress.moveToLeft(
_st.itemPadding.left(),
st::ttlItemPadding.top() + _st.itemStyle.font->height,
width);
_cancel.moveToRight(
_st.itemPadding.right(),
(_height - _cancel.height()) / 2,
width);
}, lifetime());
_progress.setClickHandlerFilter([=](const auto &...) {
callback(DownloadsAction::Retry);
return false;
});
enableMouseSelecting();
refresh(entry);
}
void Action::paint(Painter &p) {
const auto selected = isSelected();
if (selected && _st.itemBgOver->c.alpha() < 255) {
p.fillRect(0, 0, width(), _height, _st.itemBg);
}
p.fillRect(0, 0, width(), _height, selected ? _st.itemBgOver : _st.itemBg);
if (isEnabled()) {
paintRipple(p, 0, 0);
}
p.setPen(selected ? _st.itemFgOver : _st.itemFg);
_name.drawLeftElided(
p,
_st.itemPadding.left(),
st::ttlItemPadding.top(),
_textWidth,
width());
_progress.setTextColorOverride(
selected ? _st.itemFgShortcutOver->c : _st.itemFgShortcut->c);
}
void Action::prepare() {
const auto filenameWidth = _name.maxWidth();
const auto progressWidth = _progress.textMaxWidth();
const auto &padding = _st.itemPadding;
const auto goodWidth = std::max(filenameWidth, progressWidth);
// Example max width: "4000 / 4000 MB"
const auto countWidth = [&](const QString &text) {
return st::ttlItemTimerFont->width(text);
};
const auto maxProgressWidth = countWidth(tr::lng_media_save_progress(
tr::now,
lt_ready,
"0000",
lt_total,
"0000",
lt_mb,
"MB"));
const auto maxStartingWidth = countWidth(
tr::lng_bot_download_starting(tr::now));
const auto maxFailedWidth = countWidth(tr::lng_bot_download_failed(
tr::now,
lt_retry,
tr::lng_bot_download_retry(tr::now)));
const auto cancel = _cancel.width() + padding.right();
const auto paddings = padding.left() + padding.right() + cancel;
const auto w = std::clamp(
paddings + std::max({
goodWidth,
maxProgressWidth,
maxStartingWidth,
maxFailedWidth,
}),
_st.widthMin,
_st.widthMax);
_textWidth = w - paddings;
_progress.resizeToWidth(_textWidth);
setMinWidth(w);
update();
}
bool Action::isEnabled() const {
return _entry.total > 0 && _entry.ready == _entry.total;
}
void Action::handleKeyPress(not_null<QKeyEvent*> e) {
if (!isSelected()) {
return;
}
const auto key = e->key();
if (key == Qt::Key_Enter || key == Qt::Key_Return) {
setClicked(Menu::TriggeredSource::Keyboard);
}
}
void Action::refresh(const DownloadsEntry &entry) {
_entry = entry;
const auto filename = entry.path.split('/').last();
_name.setMarkedText(_st.itemStyle, { filename }, kDefaultTextOptions);
const auto progressText = (entry.total && entry.total == entry.ready)
? TextWithEntities{ FormatSizeText(entry.total) }
: entry.loading
? (entry.total
? TextWithEntities{
FormatProgressText(entry.ready, entry.total),
}
: tr::lng_bot_download_starting(tr::now, tr::marked))
: tr::lng_bot_download_failed(
tr::now,
lt_retry,
Text::Link(tr::lng_bot_download_retry(tr::now)),
tr::marked);
_progress.setMarkedText(progressText);
const auto enabled = isEnabled();
setCursor(enabled ? style::cur_pointer : style::cur_default);
_cancel.setVisible(!enabled && _entry.loading);
_progress.setAttribute(Qt::WA_TransparentForMouseEvents, enabled);
prepare();
}
} // namespace
FnMut<void(not_null<PopupMenu*>)> FillAttachBotDownloadsSubmenu(
rpl::producer<std::vector<DownloadsEntry>> content,
Fn<void(uint32, DownloadsAction)> callback) {
return [callback, moved = std::move(content)](
not_null<PopupMenu*> menu) mutable {
struct Row {
not_null<Action*> action;
uint32 id = 0;
};
struct State {
std::vector<Row> rows;
};
const auto state = menu->lifetime().make_state<State>();
std::move(
moved
) | rpl::on_next([=](
const std::vector<DownloadsEntry> &entries) {
auto found = base::flat_set<uint32>();
for (const auto &entry : entries | ranges::views::reverse) {
const auto id = entry.id;
const auto path = entry.path;
const auto i = ranges::find(state->rows, id, &Row::id);
found.emplace(id);
if (i != end(state->rows)) {
i->action->refresh(entry);
} else {
auto action = base::make_unique_q<Action>(
menu,
entry,
[=](DownloadsAction type) { callback(id, type); });
state->rows.push_back({
.action = action.get(),
.id = id,
});
menu->addAction(std::move(action));
}
}
for (auto i = begin(state->rows); i != end(state->rows);) {
if (!found.contains(i->id)) {
menu->removeAction(i - begin(state->rows));
i = state->rows.erase(i);
} else {
++i;
}
}
}, menu->lifetime());
};
}
} // namespace Ui::BotWebView

View File

@@ -0,0 +1,47 @@
/*
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
namespace Ui {
class PopupMenu;
} // namespace Ui
namespace Ui::BotWebView {
struct DownloadsProgress {
uint64 ready = 0;
uint64 total : 63 = 0;
uint64 loading : 1 = 0;
friend inline bool operator==(
const DownloadsProgress &a,
const DownloadsProgress &b) = default;
};
struct DownloadsEntry {
uint32 id = 0;
QString url;
QString path;
uint64 ready : 63 = 0;
uint64 loading : 1 = 0;
uint64 total : 63 = 0;
uint64 failed : 1 = 0;
};
enum class DownloadsAction {
Open,
Retry,
Cancel,
};
[[nodiscard]] auto FillAttachBotDownloadsSubmenu(
rpl::producer<std::vector<DownloadsEntry>> content,
Fn<void(uint32, DownloadsAction)> callback)
-> FnMut<void(not_null<PopupMenu*>)>;
} // namespace Ui::BotWebView

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,268 @@
/*
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 "base/expected.h"
#include "base/object_ptr.h"
#include "base/weak_ptr.h"
#include "base/flags.h"
#include "ui/rect_part.h"
#include "ui/round_rect.h"
#include "webview/webview_common.h"
class QJsonObject;
class QJsonValue;
namespace Ui {
class FlatLabel;
class BoxContent;
class RpWidget;
class SeparatePanel;
class IconButton;
enum class LayerOption;
using LayerOptions = base::flags<LayerOption>;
} // namespace Ui
namespace Webview {
struct Available;
} // namespace Webview
namespace Ui::BotWebView {
struct DownloadsProgress;
struct DownloadsEntry;
enum class DownloadsAction;
[[nodiscard]] TextWithEntities ErrorText(const Webview::Available &info);
enum class MenuButton {
None = 0x00,
OpenBot = 0x01,
RemoveFromMenu = 0x02,
RemoveFromMainMenu = 0x04,
ShareGame = 0x08,
};
inline constexpr bool is_flag_type(MenuButton) { return true; }
using MenuButtons = base::flags<MenuButton>;
using CustomMethodResult = base::expected<QByteArray, QString>;
struct CustomMethodRequest {
QString method;
QByteArray params;
Fn<void(CustomMethodResult)> callback;
};
struct SetEmojiStatusRequest {
uint64 customEmojiId = 0;
TimeId duration = 0;
Fn<void(QString)> callback;
};
struct DownloadFileRequest {
QString url;
QString name;
Fn<void(bool)> callback;
};
struct SendPreparedMessageRequest {
QString id = 0;
Fn<void(QString)> callback;
};
class Delegate {
public:
[[nodiscard]] virtual Webview::ThemeParams botThemeParams() = 0;
[[nodiscard]] virtual auto botDownloads(bool forceCheck = false)
-> const std::vector<DownloadsEntry> & = 0;
virtual void botDownloadsAction(uint32 id, DownloadsAction type) = 0;
virtual bool botHandleLocalUri(QString uri, bool keepOpen) = 0;
virtual void botHandleInvoice(QString slug) = 0;
virtual void botHandleMenuButton(MenuButton button) = 0;
virtual bool botValidateExternalLink(QString uri) = 0;
virtual void botOpenIvLink(QString uri) = 0;
virtual void botSendData(QByteArray data) = 0;
virtual void botSwitchInlineQuery(
std::vector<QString> chatTypes,
QString query) = 0;
virtual void botCheckWriteAccess(Fn<void(bool allowed)> callback) = 0;
virtual void botAllowWriteAccess(Fn<void(bool allowed)> callback) = 0;
virtual bool botStorageWrite(
QString key,
std::optional<QString> value) = 0;
[[nodiscard]] virtual std::optional<QString> botStorageRead(
QString key) = 0;
virtual void botStorageClear() = 0;
virtual void botRequestEmojiStatusAccess(
Fn<void(bool allowed)> callback) = 0;
virtual void botSharePhone(Fn<void(bool shared)> callback) = 0;
virtual void botInvokeCustomMethod(CustomMethodRequest request) = 0;
virtual void botSetEmojiStatus(SetEmojiStatusRequest request) = 0;
virtual void botDownloadFile(DownloadFileRequest request) = 0;
virtual void botSendPreparedMessage(
SendPreparedMessageRequest request) = 0;
virtual void botVerifyAge(int age) = 0;
virtual void botOpenPrivacyPolicy() = 0;
virtual void botClose() = 0;
};
struct Args {
QString url;
Webview::StorageId storageId;
rpl::producer<QString> title;
object_ptr<Ui::RpWidget> titleBadge = { nullptr };
rpl::producer<QString> bottom;
not_null<Delegate*> delegate;
MenuButtons menuButtons;
bool fullscreen = false;
bool allowClipboardRead = false;
rpl::producer<DownloadsProgress> downloadsProgress;
};
class Panel final : public base::has_weak_ptr {
public:
explicit Panel(Args &&args);
~Panel();
void requestActivate();
void toggleProgress(bool shown);
void showBox(object_ptr<BoxContent> box);
void showBox(
object_ptr<BoxContent> box,
LayerOptions options,
anim::type animated);
void hideLayer(anim::type animated);
void showToast(TextWithEntities &&text);
not_null<QWidget*> toastParent() const;
void showCriticalError(const TextWithEntities &text);
void showWebviewError(
const QString &text,
const Webview::Available &information);
void updateThemeParams(const Webview::ThemeParams &params);
void hideForPayment();
void invoiceClosed(const QString &slug, const QString &status);
[[nodiscard]] rpl::lifetime &lifetime();
private:
class Button;
struct Progress;
struct WebviewWithLifetime;
bool showWebview(Args &&args, const Webview::ThemeParams &params);
bool createWebview(const Webview::ThemeParams &params);
void createWebviewBottom();
void showWebviewProgress();
void hideWebviewProgress();
void setupDownloadsProgress(
not_null<RpWidget*> button,
rpl::producer<DownloadsProgress> progress,
bool fullscreen);
void setTitle(rpl::producer<QString> title);
void sendDataMessage(const QJsonObject &args);
void switchInlineQueryMessage(const QJsonObject &args);
void processSendMessageRequest(const QJsonObject &args);
void processEmojiStatusRequest(const QJsonObject &args);
void processEmojiStatusAccessRequest();
void processStorageSaveKey(const QJsonObject &args);
void processStorageGetKey(const QJsonObject &args);
void processStorageClear(const QJsonObject &args);
void processButtonMessage(
std::unique_ptr<Button> &button,
const QJsonObject &args);
void processBackButtonMessage(const QJsonObject &args);
void processSettingsButtonMessage(const QJsonObject &args);
void processHeaderColor(const QJsonObject &args);
void processBackgroundColor(const QJsonObject &args);
void processBottomBarColor(const QJsonObject &args);
void processDownloadRequest(const QJsonObject &args);
void openTgLink(const QJsonObject &args);
void openExternalLink(const QJsonObject &args);
void openInvoice(const QJsonObject &args);
void openPopup(const QJsonObject &args);
void openScanQrPopup(const QJsonObject &args);
void openShareStory(const QJsonObject &args);
void requestWriteAccess();
void replyRequestWriteAccess(bool allowed);
void requestPhone();
void replyRequestPhone(bool shared);
void invokeCustomMethod(const QJsonObject &args);
void replyCustomMethod(QJsonValue requestId, QJsonObject response);
void requestClipboardText(const QJsonObject &args);
void setupClosingBehaviour(const QJsonObject &args);
void replyDeviceStorage(
const QJsonObject &args,
const QString &event,
QJsonObject response);
void deviceStorageFailed(const QJsonObject &args, QString error);
void secureStorageFailed(const QJsonObject &args);
void createButton(std::unique_ptr<Button> &button);
void scheduleCloseWithConfirmation();
void closeWithConfirmation();
void sendViewport();
void sendSafeArea();
void sendContentSafeArea();
void sendFullScreen();
void updateColorOverrides(const Webview::ThemeParams &params);
void overrideBodyColor(std::optional<QColor> color);
using EventData = std::variant<QString, QJsonObject>;
void postEvent(const QString &event);
void postEvent(const QString &event, EventData data);
[[nodiscard]] bool allowOpenLink() const;
[[nodiscard]] bool allowClipboardQuery() const;
[[nodiscard]] bool progressWithBackground() const;
[[nodiscard]] QRect progressRect() const;
void setupProgressGeometry();
void layoutButtons();
Webview::StorageId _storageId;
const not_null<Delegate*> _delegate;
bool _closeNeedConfirmation = false;
bool _hasSettingsButton = false;
MenuButtons _menuButtons = {};
std::unique_ptr<SeparatePanel> _widget;
std::unique_ptr<WebviewWithLifetime> _webview;
std::unique_ptr<RpWidget> _webviewBottom;
QPointer<FlatLabel> _webviewBottomLabel;
rpl::variable<QString> _bottomText;
QPointer<RpWidget> _webviewParent;
std::unique_ptr<RpWidget> _bottomButtonsBg;
std::unique_ptr<Button> _mainButton;
std::unique_ptr<Button> _secondaryButton;
RectPart _secondaryPosition = RectPart::Left;
rpl::variable<int> _footerHeight = 0;
std::unique_ptr<Progress> _progress;
rpl::event_stream<> _themeUpdateForced;
std::optional<QColor> _bottomBarColor;
rpl::lifetime _headerColorLifetime;
rpl::lifetime _bodyColorLifetime;
rpl::lifetime _bottomBarColorLifetime;
rpl::event_stream<> _downloadsUpdated;
rpl::variable<bool> _fullscreen = false;
bool _layerShown : 1 = false;
bool _webviewProgress : 1 = false;
bool _themeUpdateScheduled : 1 = false;
bool _hiddenForPayment : 1 = false;
bool _closeWithConfirmationScheduled : 1 = false;
bool _allowClipboardRead : 1 = false;
bool _inBlockingRequest : 1 = false;
bool _headerColorReceived : 1 = false;
bool _bodyColorReceived : 1 = false;
bool _bottomColorReceived : 1 = false;
};
[[nodiscard]] std::unique_ptr<Panel> Show(Args &&args);
} // namespace Ui::BotWebView

View File

@@ -0,0 +1,117 @@
/*
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 "ui/chat/attach/attach_controls.h"
#include "styles/style_chat_helpers.h"
namespace Ui {
AttachControls::AttachControls()
: _rect(st::sendBoxAlbumGroupRadius, st::roundedBg) {
}
void AttachControls::paint(QPainter &p, int x, int y) {
const auto groupWidth = width();
const auto groupHeight = height();
const auto full = (_type == Type::Full);
QRect groupRect(x, y, groupWidth, groupHeight);
_rect.paint(p, groupRect);
if (full) {
const auto groupHalfWidth = groupWidth / 2;
const auto groupHalfHeight = groupHeight / 2;
const auto editRect = _vertical
? QRect(x, y, groupWidth, groupHalfHeight)
: QRect(x, y, groupHalfWidth, groupHeight);
st::sendBoxAlbumGroupButtonMediaEdit.paintInCenter(p, editRect);
const auto deleteRect = _vertical
? QRect(x, y + groupHalfHeight, groupWidth, groupHalfHeight)
: QRect(x + groupHalfWidth, y, groupHalfWidth, groupHeight);
st::sendBoxAlbumGroupButtonMediaDelete.paintInCenter(p, deleteRect);
} else if (_type == Type::EditOnly) {
st::sendBoxAlbumButtonMediaEdit.paintInCenter(p, groupRect);
}
}
int AttachControls::width() const {
return (_type == Type::Full)
? (_vertical
? st::sendBoxAlbumGroupSizeVertical.width()
: st::sendBoxAlbumGroupSize.width())
: (_type == Type::EditOnly)
? st::sendBoxAlbumSmallGroupSize.width()
: 0;
}
int AttachControls::height() const {
return (_type == Type::Full)
? (_vertical
? st::sendBoxAlbumGroupSizeVertical.height()
: st::sendBoxAlbumGroupSize.height())
: (_type == Type::EditOnly)
? st::sendBoxAlbumSmallGroupSize.height()
: 0;
}
AttachControls::Type AttachControls::type() const {
return _type;
}
bool AttachControls::vertical() const {
return _vertical;
}
void AttachControls::setType(Type type) {
if (_type != type) {
_type = type;
}
}
void AttachControls::setVertical(bool vertical) {
_vertical = vertical;
}
AttachControlsWidget::AttachControlsWidget(
not_null<RpWidget*> parent,
AttachControls::Type type)
: RpWidget(parent)
, _edit(base::make_unique_q<AbstractButton>(this))
, _delete(base::make_unique_q<AbstractButton>(this)) {
_controls.setType(type);
const auto w = _controls.width();
resize(w, _controls.height());
if (type == AttachControls::Type::Full) {
_edit->resize(w / 2, _controls.height());
_delete->resize(w / 2, _controls.height());
_edit->moveToLeft(0, 0, w);
_delete->moveToRight(0, 0, w);
} else if (type == AttachControls::Type::EditOnly) {
_edit->resize(w, _controls.height());
_edit->moveToLeft(0, 0, w);
}
paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(this);
_controls.paint(p, 0, 0);
}, lifetime());
}
rpl::producer<> AttachControlsWidget::editRequests() const {
return _edit->clicks() | rpl::to_empty;
}
rpl::producer<> AttachControlsWidget::deleteRequests() const {
return _delete->clicks() | rpl::to_empty;
}
} // namespace Ui

View File

@@ -0,0 +1,58 @@
/*
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/abstract_button.h"
#include "ui/round_rect.h"
#include "ui/rp_widget.h"
namespace Ui {
class AttachControls final {
public:
enum class Type {
Full,
EditOnly,
None,
};
AttachControls();
void paint(QPainter &p, int x, int y);
void setType(Type type);
void setVertical(bool vertical);
[[nodiscard]] int width() const;
[[nodiscard]] int height() const;
[[nodiscard]] Type type() const;
[[nodiscard]] bool vertical() const;
private:
RoundRect _rect;
Type _type = Type::Full;
bool _vertical = false;
};
class AttachControlsWidget final : public RpWidget {
public:
AttachControlsWidget(
not_null<RpWidget*> parent,
AttachControls::Type type = AttachControls::Type::Full);
[[nodiscard]] rpl::producer<> editRequests() const;
[[nodiscard]] rpl::producer<> deleteRequests() const;
private:
const base::unique_qptr<AbstractButton> _edit;
const base::unique_qptr<AbstractButton> _delete;
AttachControls _controls;
};
} // namespace Ui

View File

@@ -0,0 +1,30 @@
/*
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 "ui/chat/attach/attach_extensions.h"
#include <QtCore/QMimeDatabase>
#include <QtGui/QImageReader>
namespace Ui {
const QStringList &ImageExtensions() {
static const auto result = [] {
const auto formats = QImageReader::supportedImageFormats();
return formats | ranges::views::transform([](const auto &format) {
return '.' + format.toLower();
}) | ranges::views::filter([](const auto &format) {
const auto mimes = QMimeDatabase().mimeTypesForFileName(
u"test"_q + format);
return !mimes.isEmpty()
&& mimes.front().name().startsWith(u"image/"_q);
}) | ranges::to<QStringList>;
}();
return result;
}
} // namespace Ui

View File

@@ -0,0 +1,14 @@
/*
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
namespace Ui {
[[nodiscard]] const QStringList &ImageExtensions();
} // namespace Ui

View File

@@ -0,0 +1,115 @@
/*
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 "ui/chat/attach/attach_item_single_file_preview.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_file_origin.h"
#include "history/history_item.h"
#include "history/view/media/history_view_document.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/chat/attach/attach_prepare.h"
#include "ui/text/format_song_name.h"
#include "ui/text/format_values.h"
#include "ui/painter.h"
#include "styles/style_chat.h"
namespace Ui {
namespace {
AttachControls::Type CheckControlsType(
not_null<HistoryItem*> item,
AttachControls::Type type) {
const auto media = item->media();
Assert(media != nullptr);
return media->allowsEditMedia()
? type
: AttachControls::Type::None;
}
} // namespace
ItemSingleFilePreview::ItemSingleFilePreview(
QWidget *parent,
const style::ComposeControls &st,
not_null<HistoryItem*> item,
AttachControls::Type type)
: AbstractSingleFilePreview(parent, st, CheckControlsType(item, type)) {
const auto media = item->media();
Assert(media != nullptr);
const auto document = media->document();
Assert(document != nullptr);
_documentMedia = document->createMediaView();
_documentMedia->thumbnailWanted(item->fullId());
rpl::single(rpl::empty) | rpl::then(
document->session().downloaderTaskFinished()
) | rpl::on_next([=] {
if (_documentMedia->thumbnail()) {
_lifetimeDownload.destroy();
}
preparePreview(document);
}, _lifetimeDownload);
}
void ItemSingleFilePreview::preparePreview(not_null<DocumentData*> document) {
AbstractSingleFilePreview::Data data;
const auto preview = _documentMedia->thumbnail()
? _documentMedia->thumbnail()->original()
: QImage();
prepareThumbFor(data, preview);
data.fileIsImage = document->isImage();
data.fileIsAudio = document->isAudioFile() || document->isVoiceMessage();
if (data.fileIsImage) {
data.name = document->filename();
// data.statusText = FormatImageSizeText(preview.size()
// / preview.devicePixelRatio());
} else if (data.fileIsAudio) {
auto filename = document->filename();
auto songTitle = QString();
auto songPerformer = QString();
if (const auto song = document->song()) {
songTitle = song->title;
songPerformer = song->performer;
if (document->isSongWithCover()) {
const auto size = QSize(
st::attachPreviewLayout.thumbSize,
st::attachPreviewLayout.thumbSize);
auto thumb = QPixmap(size);
thumb.fill(Qt::transparent);
Painter p(&thumb);
HistoryView::DrawThumbnailAsSongCover(
p,
st::songCoverOverlayFg,
_documentMedia,
QRect(QPoint(), size));
data.fileThumb = std::move(thumb);
}
} else if (document->isVoiceMessage()) {
songTitle = tr::lng_media_audio(tr::now);
}
data.name = Text::FormatSongName(filename, songTitle, songPerformer)
.string();
} else {
data.name = document->filename();
}
data.statusText = FormatSizeText(document->size);
setData(data);
}
} // namespace Ui

View File

@@ -0,0 +1,41 @@
/*
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/attach/attach_abstract_single_file_preview.h"
class HistoryItem;
class DocumentData;
namespace Data {
class DocumentMedia;
} // namespace Data
namespace Ui {
struct PreparedFile;
class IconButton;
class ItemSingleFilePreview final : public AbstractSingleFilePreview {
public:
ItemSingleFilePreview(
QWidget *parent,
const style::ComposeControls &st,
not_null<HistoryItem*> item,
AttachControls::Type type);
private:
void preparePreview(not_null<DocumentData*> document);
std::shared_ptr<::Data::DocumentMedia> _documentMedia;
rpl::lifetime _lifetimeDownload;
};
} // namespace Ui

View File

@@ -0,0 +1,245 @@
/*
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 "ui/chat/attach/attach_item_single_media_preview.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_file_origin.h"
#include "data/data_photo.h"
#include "data/data_photo_media.h"
#include "data/data_session.h"
#include "data/data_streaming.h"
#include "history/history_item.h"
#include "history/view/media/history_view_document.h"
#include "main/main_session.h"
#include "media/streaming/media_streaming_document.h"
#include "media/streaming/media_streaming_instance.h"
#include "media/streaming/media_streaming_loader_local.h"
#include "media/streaming/media_streaming_player.h"
#include "styles/style_boxes.h"
namespace Ui {
namespace {
using namespace ::Media::Streaming;
} // namespace
ItemSingleMediaPreview::ItemSingleMediaPreview(
QWidget *parent,
const style::ComposeControls &st,
Fn<bool()> gifPaused,
not_null<HistoryItem*> item,
AttachControls::Type type)
: AbstractSingleMediaPreview(parent, st, type, [=](AttachActionType type) {
if (type == AttachActionType::EditCover) {
return _isVideoFile;
}
return true;
})
, _gifPaused(std::move(gifPaused))
, _isVideoFile(item->media()->document()
&& item->media()->document()->isVideoFile())
, _fullId(item->fullId()) {
const auto media = item->media();
Assert(media != nullptr);
Main::Session *session = nullptr;
if (const auto photo = media->photo()) {
_photoMedia = photo->createMediaView();
_photoMedia->wanted(Data::PhotoSize::Large, item->fullId());
session = &photo->session();
} else if (const auto document = media->document()) {
_documentMedia = document->createMediaView();
_documentMedia->thumbnailWanted(item->fullId());
session = &document->session();
if (document->isAnimation() || document->isVideoFile()) {
setAnimated(true);
prepareStreamedPreview();
}
} else {
Unexpected("Photo or document should be set.");
}
struct ThumbInfo {
bool loaded = false;
Image *image = nullptr;
};
const auto computeThumbInfo = [=]() -> ThumbInfo {
using Size = Data::PhotoSize;
if (_documentMedia) {
return { true, _documentMedia->thumbnail() };
} else if (const auto large = _photoMedia->image(Size::Large)) {
return { true, large };
} else if (const auto thumbnail = _photoMedia->image(
Size::Thumbnail)) {
return { false, thumbnail };
} else if (const auto small = _photoMedia->image(Size::Small)) {
return { false, small };
} else {
return { false, _photoMedia->thumbnailInline() };
}
};
rpl::single(rpl::empty) | rpl::then(
session->downloaderTaskFinished()
) | rpl::on_next([=] {
const auto computed = computeThumbInfo();
if (!computed.image) {
if (_documentMedia && !_documentMedia->owner()->hasThumbnail()) {
const auto size = _documentMedia->owner()->dimensions.scaled(
st::sendMediaPreviewSize,
st::confirmMaxHeight,
Qt::KeepAspectRatio);
if (!size.isEmpty()) {
auto empty = QImage(
size,
QImage::Format_ARGB32_Premultiplied);
empty.fill(Qt::black);
preparePreview(empty);
}
_lifetimeDownload.destroy();
}
return;
} else if (computed.loaded) {
_lifetimeDownload.destroy();
}
preparePreview(computed.image->original());
}, _lifetimeDownload);
}
void ItemSingleMediaPreview::prepareStreamedPreview() {
if (_streamed || !_documentMedia) {
return;
}
const auto document = _documentMedia
? _documentMedia->owner().get()
: nullptr;
if (document && document->isAnimation()) {
setupStreamedPreview(
document->owner().streaming().sharedDocument(
document,
_fullId));
}
}
void ItemSingleMediaPreview::setupStreamedPreview(
std::shared_ptr<Document> shared) {
if (!shared) {
return;
}
_streamed = std::make_unique<Instance>(
std::move(shared),
[=] { update(); });
_streamed->lockPlayer();
_streamed->player().updates(
) | rpl::on_next_error([=](Update &&update) {
handleStreamingUpdate(std::move(update));
}, [=](Error &&error) {
handleStreamingError(std::move(error));
}, _streamed->lifetime());
if (_streamed->ready()) {
streamingReady(base::duplicate(_streamed->info()));
}
checkStreamedIsStarted();
}
void ItemSingleMediaPreview::handleStreamingUpdate(Update &&update) {
v::match(update.data, [&](Information &update) {
streamingReady(std::move(update));
}, [](PreloadedVideo) {
}, [&](UpdateVideo) {
this->update();
}, [](PreloadedAudio) {
}, [](UpdateAudio) {
}, [](WaitingForData) {
}, [](SpeedEstimate) {
}, [](MutedByOther) {
}, [](Finished) {
});
}
void ItemSingleMediaPreview::handleStreamingError(Error &&error) {
}
void ItemSingleMediaPreview::streamingReady(Information &&info) {
}
void ItemSingleMediaPreview::checkStreamedIsStarted() {
if (!_streamed) {
return;
} else if (_streamed->paused()) {
_streamed->resume();
}
if (!_streamed->active() && !_streamed->failed()) {
startStreamedPlayer();
}
}
void ItemSingleMediaPreview::startStreamedPlayer() {
auto options = ::Media::Streaming::PlaybackOptions();
options.audioId = _documentMedia
? AudioMsgId(_documentMedia->owner(), _fullId)
: AudioMsgId();
options.waitForMarkAsShown = true;
//if (!_streamed->withSound) {
options.mode = ::Media::Streaming::Mode::Video;
options.loop = true;
//}
_streamed->play(options);
}
bool ItemSingleMediaPreview::supportsSpoilers() const {
return false; // We are not allowed to change existing spoiler setting.
}
bool ItemSingleMediaPreview::drawBackground() const {
return true; // A sticker can't be here.
}
bool ItemSingleMediaPreview::tryPaintAnimation(QPainter &p) {
checkStreamedIsStarted();
if (_streamed
&& _streamed->player().ready()
&& !_streamed->player().videoSize().isEmpty()) {
const auto s = QSize(previewWidth(), previewHeight());
const auto paused = _gifPaused();
auto request = ::Media::Streaming::FrameRequest();
request.outer = s * style::DevicePixelRatio();
request.resize = s * style::DevicePixelRatio();
p.drawImage(
QRect(
previewLeft(),
previewTop(),
previewWidth(),
previewHeight()),
_streamed->frame(request));
if (!paused) {
_streamed->markFrameShown();
}
return true;
}
return false;
}
bool ItemSingleMediaPreview::isAnimatedPreviewReady() const {
return _streamed != nullptr;
}
auto ItemSingleMediaPreview::sharedPhotoMedia() const
-> std::shared_ptr<::Data::PhotoMedia> {
return _photoMedia;
}
} // namespace Ui

View File

@@ -0,0 +1,72 @@
/*
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/attach/attach_abstract_single_media_preview.h"
namespace Data {
class DocumentMedia;
class PhotoMedia;
} // namespace Data
namespace Media {
namespace Streaming {
class Instance;
class Document;
struct Update;
enum class Error;
struct Information;
} // namespace Streaming
} // namespace Media
class HistoryItem;
namespace Ui {
class ItemSingleMediaPreview final : public AbstractSingleMediaPreview {
public:
ItemSingleMediaPreview(
QWidget *parent,
const style::ComposeControls &st,
Fn<bool()> gifPaused,
not_null<HistoryItem*> item,
AttachControls::Type type);
std::shared_ptr<::Data::PhotoMedia> sharedPhotoMedia() const;
protected:
bool supportsSpoilers() const override;
bool drawBackground() const override;
bool tryPaintAnimation(QPainter &p) override;
bool isAnimatedPreviewReady() const override;
private:
void prepareStreamedPreview();
void checkStreamedIsStarted();
void setupStreamedPreview(
std::shared_ptr<::Media::Streaming::Document> shared);
void handleStreamingUpdate(::Media::Streaming::Update &&update);
void handleStreamingError(::Media::Streaming::Error &&error);
void streamingReady(::Media::Streaming::Information &&info);
void startStreamedPlayer();
const Fn<bool()> _gifPaused;
const bool _isVideoFile;
const FullMsgId _fullId;
std::shared_ptr<::Data::PhotoMedia> _photoMedia;
std::shared_ptr<::Data::DocumentMedia> _documentMedia;
std::unique_ptr<::Media::Streaming::Instance> _streamed;
rpl::lifetime _lifetimeDownload;
};
} // namespace Ui

View File

@@ -0,0 +1,390 @@
/*
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 "ui/chat/attach/attach_prepare.h"
#include "ui/rp_widget.h"
#include "ui/widgets/popup_menu.h"
#include "ui/chat/attach/attach_send_files_way.h"
#include "ui/image/image_prepare.h"
#include "ui/ui_utility.h"
#include "core/mime_type.h"
namespace Ui {
namespace {
constexpr auto kMaxAlbumCount = 10;
} // namespace
PreparedFile::PreparedFile(const QString &path) : path(path) {
}
PreparedFile::PreparedFile(PreparedFile &&other) = default;
PreparedFile &PreparedFile::operator=(PreparedFile &&other) = default;
PreparedFile::~PreparedFile() = default;
bool PreparedFile::canBeInAlbumType(AlbumType album) const {
return CanBeInAlbumType(type, album);
}
bool PreparedFile::isSticker() const {
Expects(information != nullptr);
return (type == PreparedFile::Type::Photo)
&& Core::IsMimeSticker(information->filemime);
}
bool PreparedFile::isVideoFile() const {
Expects(information != nullptr);
using Video = Ui::PreparedFileInformation::Video;
return (type == PreparedFile::Type::Video)
&& v::is<Video>(information->media)
&& !v::get<Video>(information->media).isGifv;
}
bool PreparedFile::isGifv() const {
Expects(information != nullptr);
using Video = Ui::PreparedFileInformation::Video;
return (type == PreparedFile::Type::Video)
&& v::is<Video>(information->media)
&& v::get<Video>(information->media).isGifv;
}
AlbumType PreparedFile::albumType(bool sendImagesAsPhotos) const {
switch (type) {
case Type::Photo:
return sendImagesAsPhotos ? AlbumType::PhotoVideo : AlbumType::File;
case Type::Video:
return AlbumType::PhotoVideo;
case Type::Music:
return AlbumType::Music;
case Type::File:
return AlbumType::File;
case Type::None:
return AlbumType::None;
}
Unexpected("PreparedFile::type in PreparedFile::albumType().");
}
bool CanBeInAlbumType(PreparedFile::Type type, AlbumType album) {
Expects(album != AlbumType::None);
using Type = PreparedFile::Type;
switch (album) {
case AlbumType::PhotoVideo:
return (type == Type::Photo) || (type == Type::Video);
case AlbumType::Music:
return (type == Type::Music);
case AlbumType::File:
return (type == Type::Photo) || (type == Type::File);
}
Unexpected("AlbumType in CanBeInAlbumType.");
}
bool InsertTextOnImageCancel(const QString &text) {
return !text.isEmpty() && !text.startsWith(u"data:image"_q);
}
PreparedList PreparedList::Reordered(
PreparedList &&list,
std::vector<int> order) {
Expects(list.error == PreparedList::Error::None);
Expects(list.files.size() == order.size());
auto result = PreparedList(list.error, list.errorData);
result.files.reserve(list.files.size());
for (auto index : order) {
result.files.push_back(std::move(list.files[index]));
}
return result;
}
void PreparedList::mergeToEnd(PreparedList &&other, bool cutToAlbumSize) {
if (error != Error::None) {
return;
}
if (other.error != Error::None) {
error = other.error;
errorData = other.errorData;
return;
}
files.reserve(std::min(
size_t(cutToAlbumSize ? kMaxAlbumCount : INT_MAX),
files.size() + other.files.size()));
for (auto &file : other.files) {
if (cutToAlbumSize && files.size() == kMaxAlbumCount) {
break;
}
files.push_back(std::move(file));
}
}
bool PreparedList::canBeSentInSlowmode() const {
return canBeSentInSlowmodeWith(PreparedList());
}
bool PreparedList::canBeSentInSlowmodeWith(const PreparedList &other) const {
if (!filesToProcess.empty() || !other.filesToProcess.empty()) {
return false;
} else if (files.size() + other.files.size() < 2) {
return true;
} else if (files.size() + other.files.size() > kMaxAlbumCount) {
return false;
}
using Type = PreparedFile::Type;
auto &&all = ranges::views::concat(files, other.files);
const auto has = [&](Type type) {
return ranges::contains(all, type, &PreparedFile::type);
};
const auto hasNonGrouping = has(Type::None);
const auto hasPhotos = has(Type::Photo);
const auto hasFiles = has(Type::File);
const auto hasVideos = has(Type::Video);
const auto hasMusic = has(Type::Music);
// File-s and Video-s never can be grouped.
// Music-s can be grouped only with themselves.
if (hasNonGrouping) {
return false;
} else if (hasFiles) {
return !hasMusic && !hasVideos;
} else if (hasVideos) {
return !hasMusic && !hasFiles;
} else if (hasMusic) {
return !hasVideos && !hasFiles && !hasPhotos;
}
return !hasNonGrouping && (!hasFiles || !hasVideos);
}
bool PreparedList::canAddCaption(bool sendingAlbum, bool compress) const {
if (!filesToProcess.empty()
|| files.empty()
|| files.size() > kMaxAlbumCount) {
return false;
}
if (files.size() == 1) {
Assert(files.front().information != nullptr);
const auto isSticker = (!compress
&& Core::IsMimeSticker(files.front().information->filemime))
|| files.front().path.endsWith(u".tgs"_q, Qt::CaseInsensitive);
return !isSticker;
} else if (!sendingAlbum) {
return false;
}
const auto hasFiles = ranges::contains(
files,
PreparedFile::Type::File,
&PreparedFile::type);
const auto hasMusic = ranges::contains(
files,
PreparedFile::Type::Music,
&PreparedFile::type);
const auto hasNotGrouped = ranges::contains(
files,
PreparedFile::Type::None,
&PreparedFile::type);
return !hasFiles && !hasMusic && !hasNotGrouped;
}
bool PreparedList::canMoveCaption(bool sendingAlbum, bool compress) const {
if (!canAddCaption(sendingAlbum, compress)) {
return false;
} else if (files.size() != 1) {
return true;
}
const auto &file = files.front();
return (file.type == PreparedFile::Type::Video)
|| (file.type == PreparedFile::Type::Photo && compress);
}
bool PreparedList::canChangePrice(bool sendingAlbum, bool compress) const {
return canMoveCaption(sendingAlbum, compress);
}
bool PreparedList::hasGroupOption(bool slowmode) const {
if (slowmode || files.size() < 2) {
return false;
}
using Type = PreparedFile::Type;
auto lastType = Type::None;
for (const auto &file : files) {
if ((file.type == lastType)
|| (file.type == Type::Video && lastType == Type::Photo)
|| (file.type == Type::Photo && lastType == Type::Video)
|| (file.type == Type::File && lastType == Type::Photo)
|| (file.type == Type::Photo && lastType == Type::File)) {
if (lastType != Type::None) {
return true;
}
}
lastType = file.type;
}
return false;
}
bool PreparedList::hasSendImagesAsPhotosOption(bool slowmode) const {
using Type = PreparedFile::Type;
return slowmode
? ((files.size() == 1) && (files.front().type == Type::Photo))
: ranges::contains(files, Type::Photo, &PreparedFile::type);
}
bool PreparedList::canHaveEditorHintLabel() const {
for (const auto &file : files) {
if ((file.type == PreparedFile::Type::Photo)
&& !Core::IsMimeSticker(file.information->filemime)) {
return true;
}
}
return false;
}
bool PreparedList::hasSticker() const {
return ranges::any_of(files, &PreparedFile::isSticker);
}
bool PreparedList::hasSpoilerMenu(bool compress) const {
const auto allAreVideo = !ranges::any_of(files, [](const auto &f) {
using Type = Ui::PreparedFile::Type;
return (f.type != Type::Video);
});
const auto allAreMedia = !ranges::any_of(files, [](const auto &f) {
using Type = Ui::PreparedFile::Type;
return (f.type != Type::Photo) && (f.type != Type::Video);
});
return allAreVideo || (allAreMedia && compress);
}
std::shared_ptr<PreparedBundle> PrepareFilesBundle(
std::vector<PreparedGroup> groups,
SendFilesWay way,
TextWithTags caption,
bool ctrlShiftEnter) {
auto totalCount = 0;
for (const auto &group : groups) {
totalCount += group.list.files.size();
}
const auto sendComment = !caption.text.isEmpty()
&& (groups.size() != 1 || !groups.front().sentWithCaption());
return std::make_shared<PreparedBundle>(PreparedBundle{
.groups = std::move(groups),
.way = way,
.caption = std::move(caption),
.totalCount = totalCount + (sendComment ? 1 : 0),
.sendComment = sendComment,
.ctrlShiftEnter = ctrlShiftEnter,
});
}
int MaxAlbumItems() {
return kMaxAlbumCount;
}
bool ValidateThumbDimensions(int width, int height) {
return (width > 0)
&& (height > 0)
&& (width <= 20 * height)
&& (height <= 20 * width);
}
std::vector<PreparedGroup> DivideByGroups(
PreparedList &&list,
SendFilesWay way,
bool slowmode) {
const auto sendImagesAsPhotos = way.sendImagesAsPhotos();
const auto groupFiles = way.groupFiles() || slowmode;
auto group = Ui::PreparedList();
using Type = Ui::PreparedFile::Type;
auto groupType = AlbumType::None;
auto result = std::vector<PreparedGroup>();
auto pushGroup = [&] {
const auto type = (group.files.size() > 1)
? groupType
: AlbumType::None;
result.push_back(PreparedGroup{
.list = base::take(group),
.type = type,
});
};
for (auto i = 0; i != list.files.size(); ++i) {
auto &file = list.files[i];
const auto fileGroupType = (file.type == Type::Music)
? (groupFiles ? AlbumType::Music : AlbumType::None)
: (file.type == Type::Video)
? (groupFiles ? AlbumType::PhotoVideo : AlbumType::None)
: (file.type == Type::Photo)
? ((groupFiles && sendImagesAsPhotos)
? AlbumType::PhotoVideo
: (groupFiles && !sendImagesAsPhotos)
? AlbumType::File
: AlbumType::None)
: (file.type == Type::File)
? (groupFiles ? AlbumType::File : AlbumType::None)
: AlbumType::None;
if ((!group.files.empty() && groupType != fileGroupType)
|| ((groupType != AlbumType::None)
&& (group.files.size() == Ui::MaxAlbumItems()))) {
pushGroup();
}
group.files.push_back(std::move(file));
groupType = fileGroupType;
}
if (!group.files.empty()) {
pushGroup();
}
return result;
}
QPixmap PrepareSongCoverForThumbnail(QImage image, int size) {
const auto scaledSize = image.size().scaled(
size,
size,
Qt::KeepAspectRatioByExpanding);
using Option = Images::Option;
const auto ratio = style::DevicePixelRatio();
return PixmapFromImage(Images::Prepare(
std::move(image),
scaledSize * ratio,
{
.colored = &st::songCoverOverlayFg,
.options = Option::RoundCircle,
.outer = { size, 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(),
{ .options = RoundOptions(ImageRoundRadius::Large, corners) }));
}
} // namespace Ui

View File

@@ -0,0 +1,180 @@
/*
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 "editor/photo_editor_common.h"
#include "ui/chat/attach/attach_send_files_way.h"
#include "ui/rect_part.h"
#include <QtCore/QSemaphore>
#include <deque>
namespace Ui {
class RpWidget;
class SendFilesWay;
struct PreparedFileInformation {
struct Image {
QImage data;
QByteArray bytes;
QByteArray format;
bool animated = false;
Editor::PhotoModifications modifications;
};
struct Song {
crl::time duration = -1;
QString title;
QString performer;
QImage cover;
};
struct Video {
bool isGifv = false;
bool isWebmSticker = false;
bool supportsStreaming = false;
crl::time duration = -1;
QImage thumbnail;
};
QString filemime;
std::variant<v::null_t, Image, Song, Video> media;
};
enum class AlbumType {
None,
PhotoVideo,
Music,
File,
};
struct PreparedFile {
// File-s can be grouped if 'groupFiles'.
// File-s + Photo-s can be grouped if 'groupFiles && !sendImagesAsPhotos'.
// Photo-s can be grouped if 'groupFiles'.
// Photo-s + Video-s can be grouped if 'groupFiles && sendImagesAsPhotos'.
// Video-s can be grouped if 'groupFiles'.
// Music-s can be grouped if 'groupFiles'.
enum class Type {
None,
Photo,
Video,
Music,
File,
};
PreparedFile(const QString &path);
PreparedFile(PreparedFile &&other);
PreparedFile &operator=(PreparedFile &&other);
~PreparedFile();
[[nodiscard]] bool canBeInAlbumType(AlbumType album) const;
[[nodiscard]] AlbumType albumType(bool sendImagesAsPhotos) const;
[[nodiscard]] bool isSticker() const;
[[nodiscard]] bool isVideoFile() const;
[[nodiscard]] bool isGifv() const;
QString path;
QByteArray content;
int64 size = 0;
std::unique_ptr<PreparedFileInformation> information;
std::unique_ptr<PreparedFile> videoCover;
QImage preview;
QSize shownDimensions;
QSize originalDimensions;
Type type = Type::File;
bool spoiler = false;
};
[[nodiscard]] bool CanBeInAlbumType(PreparedFile::Type type, AlbumType album);
[[nodiscard]] bool InsertTextOnImageCancel(const QString &text);
struct PreparedList {
enum class Error {
None,
NonLocalUrl,
Directory,
EmptyFile,
TooLargeFile,
};
PreparedList() = default;
PreparedList(Error error, QString errorData)
: error(error)
, errorData(errorData) {
}
PreparedList(PreparedList &&other) = default;
PreparedList &operator=(PreparedList &&other) = default;
[[nodiscard]] static PreparedList Reordered(
PreparedList &&list,
std::vector<int> order);
void mergeToEnd(PreparedList &&other, bool cutToAlbumSize = false);
[[nodiscard]] bool canAddCaption(bool sendingAlbum, bool compress) const;
[[nodiscard]] bool canMoveCaption(
bool sendingAlbum,
bool compress) const;
[[nodiscard]] bool canChangePrice(
bool sendingAlbum,
bool compress) const;
[[nodiscard]] bool canBeSentInSlowmode() const;
[[nodiscard]] bool canBeSentInSlowmodeWith(
const PreparedList &other) const;
[[nodiscard]] bool hasGroupOption(bool slowmode) const;
[[nodiscard]] bool hasSendImagesAsPhotosOption(bool slowmode) const;
[[nodiscard]] bool canHaveEditorHintLabel() const;
[[nodiscard]] bool hasSticker() const;
[[nodiscard]] bool hasSpoilerMenu(bool compress) const;
Error error = Error::None;
QString errorData;
std::vector<PreparedFile> files;
std::deque<PreparedFile> filesToProcess;
std::optional<bool> overrideSendImagesAsPhotos;
};
struct PreparedGroup {
PreparedList list;
AlbumType type = AlbumType::None;
[[nodiscard]] bool sentWithCaption() const {
return (list.files.size() == 1)
|| (type == AlbumType::PhotoVideo);
}
};
[[nodiscard]] std::vector<PreparedGroup> DivideByGroups(
PreparedList &&list,
SendFilesWay way,
bool slowmode);
struct PreparedBundle {
std::vector<PreparedGroup> groups;
SendFilesWay way;
TextWithTags caption;
int totalCount = 0;
bool sendComment = false;
bool ctrlShiftEnter = false;
};
[[nodiscard]] std::shared_ptr<PreparedBundle> PrepareFilesBundle(
std::vector<PreparedGroup> groups,
SendFilesWay way,
TextWithTags caption,
bool ctrlShiftEnter);
[[nodiscard]] int MaxAlbumItems();
[[nodiscard]] bool ValidateThumbDimensions(int width, int height);
[[nodiscard]] QPixmap PrepareSongCoverForThumbnail(QImage image, int size);
[[nodiscard]] QPixmap BlurredPreviewFromPixmap(
QPixmap pixmap,
RectParts corners);
} // namespace Ui

View File

@@ -0,0 +1,69 @@
/*
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 "ui/chat/attach/attach_send_files_way.h"
namespace Ui {
void SendFilesWay::setSendImagesAsPhotos(bool value) {
if (value) {
_flags |= Flag::SendImagesAsPhotos;
} else {
if (hasCompressedStickers()) {
setGroupFiles(false);
}
_flags &= ~Flag::SendImagesAsPhotos;
}
}
void SendFilesWay::setGroupFiles(bool value) {
if (value) {
_flags |= Flag::GroupFiles;
if (hasCompressedStickers()) {
setSendImagesAsPhotos(true);
}
} else {
_flags &= ~Flag::GroupFiles;
}
}
void SendFilesWay::setHasCompressedStickers(bool value) {
if (value) {
_flags |= Flag::HasCompressedStickers;
} else {
_flags &= ~Flag::HasCompressedStickers;
}
}
//enum class SendFilesWay { // Old way. Serialize should be compatible.
// Album,
// Photos,
// Files,
//};
int32 SendFilesWay::serialize() const {
auto result = (sendImagesAsPhotos() && groupFiles())
? int32(0)
: sendImagesAsPhotos()
? int32(1)
: groupFiles()
? int32(3)
: int32(2);
return result;
}
std::optional<SendFilesWay> SendFilesWay::FromSerialized(int32 value) {
if (value < 0 || value > 3) {
return std::nullopt;
}
auto result = SendFilesWay();
result.setGroupFiles((value == 0) || (value == 3));
result.setSendImagesAsPhotos((value == 0) || (value == 1));
return result;
}
} // namespace Ui

View File

@@ -0,0 +1,80 @@
/*
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 "base/flags.h"
namespace Ui {
enum class AttachActionType {
ToggleSpoiler,
EditCover,
ClearCover,
};
enum class AttachButtonType {
Edit,
Delete,
Modify,
None,
};
class SendFilesWay final {
public:
[[nodiscard]] bool groupFiles() const {
return (_flags & Flag::GroupFiles) != 0;
}
[[nodiscard]] bool sendImagesAsPhotos() const {
return (_flags & Flag::SendImagesAsPhotos) != 0;
}
void setGroupFiles(bool value);
void setSendImagesAsPhotos(bool value);
void setHasCompressedStickers(bool value);
[[nodiscard]] inline bool operator<(const SendFilesWay &other) const {
return _flags < other._flags;
}
[[nodiscard]] inline bool operator>(const SendFilesWay &other) const {
return other < *this;
}
[[nodiscard]] inline bool operator<=(const SendFilesWay &other) const {
return !(other < *this);
}
[[nodiscard]] inline bool operator>=(const SendFilesWay &other) const {
return !(*this < other);
}
[[nodiscard]] inline bool operator==(const SendFilesWay &other) const {
return _flags == other._flags;
}
[[nodiscard]] inline bool operator!=(const SendFilesWay &other) const {
return !(*this == other);
}
[[nodiscard]] int32 serialize() const;
[[nodiscard]] static std::optional<SendFilesWay> FromSerialized(
int32 value);
private:
[[nodiscard]] bool hasCompressedStickers() const {
return (_flags & Flag::HasCompressedStickers) != 0;
}
enum class Flag : uchar {
GroupFiles = (1 << 0),
SendImagesAsPhotos = (1 << 1),
HasCompressedStickers = (1 << 2),
Default = GroupFiles | SendImagesAsPhotos,
};
friend inline constexpr bool is_flag_type(Flag) { return true; };
base::flags<Flag> _flags = Flag::Default;
};
} // namespace Ui

View File

@@ -0,0 +1,80 @@
/*
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 "ui/chat/attach/attach_single_file_preview.h"
#include "ui/chat/attach/attach_prepare.h"
#include "ui/text/format_song_name.h"
#include "ui/text/format_values.h"
#include "ui/ui_utility.h"
#include "core/mime_type.h"
#include "styles/style_chat.h"
#include <QtCore/QFileInfo>
namespace Ui {
SingleFilePreview::SingleFilePreview(
QWidget *parent,
const style::ComposeControls &st,
const PreparedFile &file,
AttachControls::Type type)
: AbstractSingleFilePreview(parent, st, type) {
preparePreview(file);
}
void SingleFilePreview::preparePreview(const PreparedFile &file) {
AbstractSingleFilePreview::Data data;
auto preview = QImage();
if (const auto image = std::get_if<PreparedFileInformation::Image>(
&file.information->media)) {
preview = image->data;
} else if (const auto video = std::get_if<PreparedFileInformation::Video>(
&file.information->media)) {
preview = video->thumbnail;
}
prepareThumbFor(data, preview);
const auto filepath = file.path;
if (filepath.isEmpty()) {
auto filename = "image.png";
data.name = filename;
data.statusText = FormatImageSizeText(file.originalDimensions);
data.fileIsImage = true;
} else {
auto fileinfo = QFileInfo(filepath);
auto filename = fileinfo.fileName();
data.fileIsImage = Core::FileIsImage(
filename,
Core::MimeTypeForFile(fileinfo).name());
auto songTitle = QString();
auto songPerformer = QString();
if (file.information) {
if (const auto song = std::get_if<PreparedFileInformation::Song>(
&file.information->media)) {
songTitle = song->title;
songPerformer = song->performer;
data.fileIsAudio = true;
if (auto cover = song->cover; !cover.isNull()) {
data.fileThumb = Ui::PrepareSongCoverForThumbnail(
cover,
st::attachPreviewLayout.thumbSize);
}
}
}
data.name = Text::FormatSongName(filename, songTitle, songPerformer)
.string();
data.statusText = FormatSizeText(fileinfo.size());
}
setData(data);
}
} // namespace Ui

View File

@@ -0,0 +1,29 @@
/*
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/attach/attach_abstract_single_file_preview.h"
namespace Ui {
struct PreparedFile;
class SingleFilePreview final : public AbstractSingleFilePreview {
public:
SingleFilePreview(
QWidget *parent,
const style::ComposeControls &st,
const PreparedFile &file,
AttachControls::Type type = AttachControls::Type::Full);
private:
void preparePreview(const PreparedFile &file);
};
} // namespace Ui

View File

@@ -0,0 +1,168 @@
/*
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 "ui/chat/attach/attach_single_media_preview.h"
#include "editor/photo_editor_common.h"
#include "ui/chat/attach/attach_prepare.h"
#include "core/mime_type.h"
#include "lottie/lottie_single_player.h"
namespace Ui {
SingleMediaPreview *SingleMediaPreview::Create(
QWidget *parent,
const style::ComposeControls &st,
Fn<bool()> gifPaused,
const PreparedFile &file,
Fn<bool(AttachActionType)> actionAllowed,
AttachControls::Type type) {
auto preview = QImage();
auto animated = false;
auto animationPreview = false;
auto hasModifications = false;
if (const auto image = std::get_if<PreparedFileInformation::Image>(
&file.information->media)) {
preview = Editor::ImageModified(image->data, image->modifications);
animated = animationPreview = image->animated;
hasModifications = !image->modifications.empty();
} else if (const auto video = std::get_if<PreparedFileInformation::Video>(
&file.information->media)) {
preview = file.videoCover
? file.videoCover->preview
: video->thumbnail;
animated = true;
animationPreview = video->isGifv;
}
if (preview.isNull()) {
return nullptr;
} else if (!animated
&& !ValidateThumbDimensions(preview.width(), preview.height())
&& !hasModifications) {
return nullptr;
}
return CreateChild<SingleMediaPreview>(
parent,
st,
std::move(gifPaused),
preview,
animated,
Core::IsMimeSticker(file.information->filemime),
file.spoiler,
animationPreview ? file.path : QString(),
type,
std::move(actionAllowed));
}
SingleMediaPreview::SingleMediaPreview(
QWidget *parent,
const style::ComposeControls &st,
Fn<bool()> gifPaused,
QImage preview,
bool animated,
bool sticker,
bool spoiler,
const QString &animatedPreviewPath,
AttachControls::Type type,
Fn<bool(AttachActionType)> actionAllowed)
: AbstractSingleMediaPreview(parent, st, type, std::move(actionAllowed))
, _gifPaused(std::move(gifPaused))
, _sticker(sticker) {
Expects(!preview.isNull());
setAnimated(animated);
preparePreview(preview);
prepareAnimatedPreview(animatedPreviewPath, animated);
setSpoiler(spoiler);
}
bool SingleMediaPreview::supportsSpoilers() const {
return !_sticker || sendWay().sendImagesAsPhotos();
}
bool SingleMediaPreview::drawBackground() const {
return !_sticker;
}
bool SingleMediaPreview::tryPaintAnimation(QPainter &p) {
if (_gifPreview && _gifPreview->started()) {
const auto paused = _gifPaused();
const auto frame = _gifPreview->current({
.frame = QSize(previewWidth(), previewHeight()),
}, paused ? 0 : crl::now());
p.drawImage(previewLeft(), previewTop(), frame);
return true;
} else if (_lottiePreview && _lottiePreview->ready()) {
const auto frame = _lottiePreview->frame();
const auto size = frame.size() / style::DevicePixelRatio();
p.drawImage(
QRect(
previewLeft() + (previewWidth() - size.width()) / 2,
(previewHeight() - size.height()) / 2,
size.width(),
size.height()),
frame);
_lottiePreview->markFrameShown();
return true;
}
return false;
}
bool SingleMediaPreview::isAnimatedPreviewReady() const {
return _gifPreview || _lottiePreview;
}
void SingleMediaPreview::prepareAnimatedPreview(
const QString &animatedPreviewPath,
bool animated) {
if (_sticker && animated) {
const auto box = QSize(previewWidth(), previewHeight())
* style::DevicePixelRatio();
_lottiePreview = std::make_unique<Lottie::SinglePlayer>(
Lottie::ReadContent(QByteArray(), animatedPreviewPath),
Lottie::FrameRequest{ box });
_lottiePreview->updates(
) | rpl::on_next([=] {
update();
}, lifetime());
} else if (!animatedPreviewPath.isEmpty()) {
auto callback = [=](Media::Clip::Notification notification) {
clipCallback(notification);
};
_gifPreview = Media::Clip::MakeReader(
animatedPreviewPath,
std::move(callback));
}
}
void SingleMediaPreview::clipCallback(
Media::Clip::Notification notification) {
using namespace Media::Clip;
switch (notification) {
case Notification::Reinit: {
if (_gifPreview && _gifPreview->state() == State::Error) {
_gifPreview.setBad();
}
if (_gifPreview && _gifPreview->ready() && !_gifPreview->started()) {
_gifPreview->start({
.frame = QSize(previewWidth(), previewHeight()),
});
}
update();
} break;
case Notification::Repaint: {
if (_gifPreview && !_gifPreview->currentDisplayed()) {
update();
}
} break;
}
}
} // namespace Ui

View File

@@ -0,0 +1,62 @@
/*
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/attach/attach_abstract_single_media_preview.h"
#include "media/clip/media_clip_reader.h"
namespace Lottie {
class SinglePlayer;
} // namespace Lottie
namespace Ui {
struct PreparedFile;
class SingleMediaPreview final : public AbstractSingleMediaPreview {
public:
static SingleMediaPreview *Create(
QWidget *parent,
const style::ComposeControls &st,
Fn<bool()> gifPaused,
const PreparedFile &file,
Fn<bool(AttachActionType)> actionAllowed,
AttachControls::Type type = AttachControls::Type::Full);
SingleMediaPreview(
QWidget *parent,
const style::ComposeControls &st,
Fn<bool()> gifPaused,
QImage preview,
bool animated,
bool sticker,
bool spoiler,
const QString &animatedPreviewPath,
AttachControls::Type type,
Fn<bool(AttachActionType)> actionAllowed);
protected:
bool supportsSpoilers() const override;
bool drawBackground() const override;
bool tryPaintAnimation(QPainter &p) override;
bool isAnimatedPreviewReady() const override;
private:
void prepareAnimatedPreview(
const QString &animatedPreviewPath,
bool animated);
void clipCallback(Media::Clip::Notification notification);
const Fn<bool()> _gifPaused;
const bool _sticker = false;
Media::Clip::ReaderPointer _gifPreview;
std::unique_ptr<Lottie::SinglePlayer> _lottiePreview;
};
} // namespace Ui