Send files grouped in albums, show captions.

This commit is contained in:
John Preston 2020-10-16 20:48:27 +03:00
parent 86612f0a67
commit 85d08c8f52
18 changed files with 292 additions and 99 deletions

View file

@ -4232,7 +4232,9 @@ void ApiWrap::sendFiles(
if (album) {
switch (file.type) {
case Ui::PreparedFile::AlbumType::Photo:
type = SendMediaType::Photo;
type = (type == SendMediaType::File)
? type
: SendMediaType::Photo;
break;
case Ui::PreparedFile::AlbumType::Video:
case Ui::PreparedFile::AlbumType::File:

View file

@ -763,6 +763,9 @@ void SendFilesBox::addPreparedAsyncFile(Ui::PreparedFile &&file) {
if (_list.files.size() > count) {
refreshAllAfterChanges(count);
}
if (!_preparing && _whenReadySend) {
_whenReadySend();
}
}
void SendFilesBox::addFile(Ui::PreparedFile &&file) {
@ -893,6 +896,12 @@ void SendFilesBox::send(
&& !options.scheduled) {
return sendScheduled();
}
if (_preparing) {
_whenReadySend = [=] {
send(options, ctrlShiftEnter);
};
return;
}
auto way = _sendWay.current();
auto oldWay = Core::App().settings().sendFilesWay();

View file

@ -187,6 +187,7 @@ private:
object_ptr<Ui::ScrollArea> _scroll;
QPointer<Ui::VerticalLayout> _inner;
std::vector<Block> _blocks;
Fn<void()> _whenReadySend;
bool _preparing = false;
int _lastScrollTop = 0;

View file

@ -479,7 +479,14 @@ Storage::SharedMediaTypesMask MediaFile::sharedMediaTypes() const {
}
bool MediaFile::canBeGrouped() const {
return _document->isVideoFile() || _document->isSong();
if (_document->sticker() || _document->isAnimation()) {
return false;
} else if (_document->isVideoFile()) {
return true;
} else if (_document->isTheme() && _document->hasThumbnail()) {
return false;
}
return true;
}
bool MediaFile::hasReplyPreview() const {
@ -704,7 +711,10 @@ std::unique_ptr<HistoryView::Media> MediaFile::createView(
message,
_document);
}
return std::make_unique<HistoryView::Document>(message, _document);
return std::make_unique<HistoryView::Document>(
message,
realParent,
_document);
}
MediaContact::MediaContact(

View file

@ -4177,22 +4177,12 @@ bool HistoryWidget::confirmSendingFiles(
TextWithTags &&caption,
Api::SendOptions options,
bool ctrlShiftEnter) {
if (showSendingFilesError(list)) {
return;
}
const auto type = way.sendImagesAsPhotos()
? SendMediaType::Photo
: SendMediaType::File;
const auto album = way.groupMediaInAlbums() // #TODO files
? std::make_shared<SendingAlbum>()
: nullptr;
uploadFilesAfterConfirmation(
sendingFilesConfirmed(
std::move(list),
type,
way,
std::move(caption),
replyToId(),
options,
album);
ctrlShiftEnter);
}));
box->setCancelledCallback(crl::guard(this, [=] {
_field->setTextWithTags(text);
@ -4214,6 +4204,73 @@ bool HistoryWidget::confirmSendingFiles(
return true;
}
void HistoryWidget::sendingFilesConfirmed(
Ui::PreparedList &&list,
Ui::SendFilesWay way,
TextWithTags &&caption,
Api::SendOptions options,
bool ctrlShiftEnter) {
Expects(list.filesToProcess.empty());
if (showSendingFilesError(list)) {
return;
}
const auto slowmode = _peer->slowmodeApplied();
const auto sendImagesAsPhotos = way.sendImagesAsPhotos();
const auto sendType = sendImagesAsPhotos
? SendMediaType::Photo
: SendMediaType::File;
const auto groupMedia = way.groupMediaInAlbums() || slowmode;
const auto groupFiles = way.groupFiles() || slowmode;
auto group = Ui::PreparedList();
// For groupType Type::Video means media album,
// Type::File means file album,
// Type::None means no grouping.
using Type = Ui::PreparedFile::AlbumType;
auto groupType = Type::None;
const auto reply = replyToId();
auto sendGroup = [&] {
if (group.files.empty()) {
return;
}
const auto album = (groupType == Type::None)
? nullptr
: std::make_shared<SendingAlbum>();
uploadFilesAfterConfirmation(
base::take(group),
sendType,
base::take(caption),
reply,
options,
std::move(album));
};
for (auto i = 0; i != list.files.size(); ++i) {
auto &file = list.files[i];
const auto fileGroupType = (file.type == Type::Video)
? (groupMedia ? Type::Video : Type::None)
: (file.type == Type::Photo)
? ((groupMedia && sendImagesAsPhotos)
? Type::Video
: (groupFiles && !sendImagesAsPhotos)
? Type::File
: Type::None)
: (file.type == Type::File)
? (groupFiles ? Type::File : Type::None)
: Type::None;
if ((!group.files.empty() && groupType != fileGroupType)
|| ((groupType != Type::None)
&& (group.files.size() == Ui::MaxAlbumItems()))) {
sendGroup();
}
group.files.push_back(std::move(file));
groupType = fileGroupType;
}
sendGroup();
}
bool HistoryWidget::confirmSendingFiles(
QImage &&image,
QByteArray &&content,

View file

@ -66,6 +66,7 @@ class LinkButton;
class RoundButton;
class PinnedBar;
struct PreparedList;
class SendFilesWay;
namespace Toast {
class Instance;
} // namespace Toast
@ -425,6 +426,13 @@ private:
const QString &insertTextOnCancel = QString());
bool showSendingFilesError(const Ui::PreparedList &list) const;
void sendingFilesConfirmed(
Ui::PreparedList &&list,
Ui::SendFilesWay way,
TextWithTags &&caption,
Api::SendOptions options,
bool ctrlShiftEnter);
void uploadFile(const QByteArray &fileContent, SendMediaType type);
void uploadFilesAfterConfirmation(

View file

@ -16,7 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_item.h"
#include "history/history.h"
#include "apiwrap.h"
#include "styles/style_history.h"
#include "styles/style_chat.h"
namespace HistoryView {
namespace {

View file

@ -65,8 +65,9 @@ constexpr auto kAudioVoiceMsgUpdateView = crl::time(100);
Document::Document(
not_null<Element*> parent,
not_null<HistoryItem*> realParent,
not_null<DocumentData*> document)
: File(parent, parent->data())
: File(parent, realParent)
, _data(document) {
const auto item = parent->data();
auto caption = createCaption();
@ -152,7 +153,7 @@ QSize Document::countOptimalSize() {
const auto item = _parent->data();
auto captioned = Get<HistoryDocumentCaptioned>();
if (_parent->media() != this) {
if (_parent->media() != this && !_realParent->groupId()) {
if (captioned) {
RemoveComponents(HistoryDocumentCaptioned::Bit());
captioned = nullptr;
@ -508,7 +509,7 @@ void Document::draw(
}
}
if (mode == LayoutMode::Full) {
if (mode != LayoutMode::GroupedLast) {
if (auto captioned = Get<HistoryDocumentCaptioned>()) {
p.setPen(outbg ? (selected ? st::historyTextOutFgSelected : st::historyTextOutFg) : (selected ? st::historyTextInFgSelected : st::historyTextInFg));
captioned->_caption.draw(p, st::msgPadding.left(), bottom, captionw, style::al_left, 0, -1, selection);
@ -682,7 +683,7 @@ TextState Document::textState(
}
auto painth = layout.height();
if (mode == LayoutMode::Full) {
if (mode != LayoutMode::GroupedLast) {
if (const auto captioned = Get<HistoryDocumentCaptioned>()) {
if (point.y() >= bottom) {
result = TextState(_parent, captioned->_caption.getState(
@ -859,10 +860,43 @@ bool Document::hideForwardedFrom() const {
return _data->isSong();
}
QSize Document::sizeForGrouping() const {
const auto height = st::msgFilePadding.top()
QSize Document::sizeForGroupingOptimal(int maxWidth, bool last) const {
auto height = Has<HistoryDocumentThumbed>()
? (st::msgFileThumbPadding.top()
+ st::msgFileThumbSize
+ st::msgFileThumbPadding.bottom())
: (st::msgFilePadding.top()
+ st::msgFileSize
+ st::msgFilePadding.bottom();
+ st::msgFilePadding.bottom());
if (!last) {
if (const auto captioned = Get<HistoryDocumentCaptioned>()) {
auto captionw = maxWidth
- st::msgPadding.left()
- st::msgPadding.right();
height += captioned->_caption.countHeight(captionw);
}
}
return { maxWidth, height };
}
QSize Document::sizeForGrouping(int width, bool last) const {
auto height = Has<HistoryDocumentThumbed>()
? (st::msgFileThumbPadding.top()
+ st::msgFileThumbSize
+ st::msgFileThumbPadding.bottom())
: (st::msgFilePadding.top()
+ st::msgFileSize
+ st::msgFilePadding.bottom());
if (!last) {
if (const auto captioned = Get<HistoryDocumentCaptioned>()) {
auto captionw = width
- st::msgPadding.left()
- st::msgPadding.right();
height += captioned->_caption.countHeight(captionw);
}
}
return { maxWidth(), height };
}
@ -875,9 +909,15 @@ void Document::drawGrouped(
RectParts sides,
RectParts corners,
not_null<uint64*> cacheKey,
not_null<QPixmap*> cache) const {
not_null<QPixmap*> cache,
bool last) const {
p.translate(geometry.topLeft());
draw(p, geometry.width(), selection, ms, LayoutMode::Grouped);
draw(
p,
geometry.width(),
selection,
ms,
last ? LayoutMode::GroupedLast : LayoutMode::Grouped);
p.translate(-geometry.topLeft());
}
@ -885,9 +925,14 @@ TextState Document::getStateGrouped(
const QRect &geometry,
RectParts sides,
QPoint point,
StateRequest request) const {
StateRequest request,
bool last) const {
point -= geometry.topLeft();
return textState(point, geometry.size(), request, LayoutMode::Grouped);
return textState(
point,
geometry.size(),
request,
last ? LayoutMode::GroupedLast : LayoutMode::Grouped);
}
bool Document::voiceProgressAnimationCallback(crl::time now) {
@ -952,7 +997,7 @@ void Document::refreshParentId(not_null<HistoryItem*> realParent) {
}
void Document::parentTextUpdated() {
auto caption = (_parent->media() == this)
auto caption = (_parent->media() == this || _realParent->groupId())
? createCaption()
: Ui::Text::String();
if (!caption.isEmpty()) {
@ -980,7 +1025,7 @@ Ui::Text::String Document::createCaption() {
? DocumentTimestampLinkBase(_data, _realParent->fullId())
: QString();
return File::createCaption(
_parent->data(),
_realParent,
timestampLinksDuration,
timestampLinkBase);
}

View file

@ -30,6 +30,7 @@ class Document final
public:
Document(
not_null<Element*> parent,
not_null<HistoryItem*> realParent,
not_null<DocumentData*> document);
~Document();
@ -61,7 +62,8 @@ public:
QMargins bubbleMargins() const override;
bool hideForwardedFrom() const override;
QSize sizeForGrouping() const override;
QSize sizeForGroupingOptimal(int maxWidth, bool last) const override;
QSize sizeForGrouping(int width, bool last) const override;
void drawGrouped(
Painter &p,
const QRect &clip,
@ -71,12 +73,14 @@ public:
RectParts sides,
RectParts corners,
not_null<uint64*> cacheKey,
not_null<QPixmap*> cache) const override;
not_null<QPixmap*> cache,
bool last) const override;
TextState getStateGrouped(
const QRect &geometry,
RectParts sides,
QPoint point,
StateRequest request) const override;
StateRequest request,
bool last) const override;
bool voiceProgressAnimationCallback(crl::time now);
@ -102,6 +106,7 @@ private:
enum class LayoutMode {
Full,
Grouped,
GroupedLast,
};
void draw(

View file

@ -884,7 +884,11 @@ bool Gif::fullFeaturedGrouped(RectParts sides) const {
return (sides & RectPart::Left) && (sides & RectPart::Right);
}
QSize Gif::sizeForGrouping() const {
QSize Gif::sizeForGroupingOptimal(int maxWidth, bool last) const {
return sizeForAspectRatio();
}
QSize Gif::sizeForGrouping(int width, bool last) const {
return sizeForAspectRatio();
}
@ -897,7 +901,8 @@ void Gif::drawGrouped(
RectParts sides,
RectParts corners,
not_null<uint64*> cacheKey,
not_null<QPixmap*> cache) const {
not_null<QPixmap*> cache,
bool last) const {
ensureDataMediaCreated();
const auto item = _parent->data();
const auto loaded = dataLoaded();
@ -1085,7 +1090,8 @@ TextState Gif::getStateGrouped(
const QRect &geometry,
RectParts sides,
QPoint point,
StateRequest request) const {
StateRequest request,
bool last) const {
if (!geometry.contains(point)) {
return {};
}

View file

@ -69,7 +69,8 @@ public:
}
bool fullFeaturedGrouped(RectParts sides) const;
QSize sizeForGrouping() const override;
QSize sizeForGroupingOptimal(int maxWidth, bool last) const override;
QSize sizeForGrouping(int width, bool last) const override;
void drawGrouped(
Painter &p,
const QRect &clip,
@ -79,12 +80,14 @@ public:
RectParts sides,
RectParts corners,
not_null<uint64*> cacheKey,
not_null<QPixmap*> cache) const override;
not_null<QPixmap*> cache,
bool last) const override;
TextState getStateGrouped(
const QRect &geometry,
RectParts sides,
QPoint point,
StateRequest request) const override;
StateRequest request,
bool last) const override;
void stopAnimation() override;
void checkAnimation() override;

View file

@ -182,7 +182,8 @@ TextState Media::getStateGrouped(
const QRect &geometry,
RectParts sides,
QPoint point,
StateRequest request) const {
StateRequest request,
bool last) const {
Unexpected("Grouping method call.");
}

View file

@ -163,7 +163,12 @@ public:
virtual void checkAnimation() {
}
[[nodiscard]] virtual QSize sizeForGrouping() const {
[[nodiscard]] virtual QSize sizeForGroupingOptimal(
int maxWidth,
bool last) const {
Unexpected("Grouping method call.");
}
[[nodiscard]] virtual QSize sizeForGrouping(int width, bool last) const {
Unexpected("Grouping method call.");
}
virtual void drawGrouped(
@ -175,14 +180,16 @@ public:
RectParts sides,
RectParts corners,
not_null<uint64*> cacheKey,
not_null<QPixmap*> cache) const {
not_null<QPixmap*> cache,
bool last) const {
Unexpected("Grouping method call.");
}
[[nodiscard]] virtual TextState getStateGrouped(
const QRect &geometry,
RectParts sides,
QPoint point,
StateRequest request) const;
StateRequest request,
bool last) const;
[[nodiscard]] virtual bool animating() const {
return false;

View file

@ -80,7 +80,7 @@ std::unique_ptr<Media> CreateAttach(
document,
webpageUrl);
}
return std::make_unique<Document>(parent, document);
return std::make_unique<Document>(parent, parent->data(), document);
} else if (photo) {
return std::make_unique<Photo>(
parent,

View file

@ -94,8 +94,8 @@ GroupedMedia::~GroupedMedia() {
GroupedMedia::Mode GroupedMedia::DetectMode(not_null<Data::Media*> media) {
const auto document = media->document();
return (document && document->isSong())
? Mode::Playlist
return (document && !document->isVideoFile())
? Mode::Column
: Mode::Grid;
}
@ -107,11 +107,20 @@ QSize GroupedMedia::countOptimalSize() {
}
std::vector<QSize> sizes;
sizes.reserve(_parts.size());
const auto partsCount = _parts.size();
sizes.reserve(partsCount);
auto maxWidth = 0;
if (_mode == Mode::Column) {
for (const auto &part : _parts) {
const auto &media = part.content;
media->initDimensions();
sizes.push_back(media->sizeForGrouping());
accumulate_max(maxWidth, media->maxWidth());
}
}
auto index = 0;
for (const auto &part : _parts) {
const auto last = (++index == partsCount);
sizes.push_back(part.content->sizeForGroupingOptimal(maxWidth, last));
}
const auto layout = (_mode == Mode::Grid)
@ -123,7 +132,6 @@ QSize GroupedMedia::countOptimalSize() {
: LayoutPlaylist(sizes);
Assert(layout.size() == _parts.size());
auto maxWidth = 0;
auto minHeight = 0;
for (auto i = 0, count = int(layout.size()); i != count; ++i) {
const auto &item = layout[i];
@ -146,10 +154,19 @@ QSize GroupedMedia::countOptimalSize() {
QSize GroupedMedia::countCurrentSize(int newWidth) {
accumulate_min(newWidth, maxWidth());
auto newHeight = 0;
if (newWidth < st::historyGroupWidthMin) {
if (_mode == Mode::Grid && newWidth < st::historyGroupWidthMin) {
return { newWidth, newHeight };
} else if (_mode == Mode::Column) {
auto index = 0;
auto top = 0;
for (auto &part : _parts) {
const auto last = (++index == _parts.size());
const auto size = part.content->sizeForGrouping(newWidth, last);
part.geometry = QRect(0, top, newWidth, size.height());
top += size.height();
}
newHeight = top;
} else {
const auto initialSpacing = st::historyGroupSkip;
const auto factor = newWidth / float64(maxWidth());
const auto scale = [&](int value) {
@ -181,7 +198,7 @@ QSize GroupedMedia::countCurrentSize(int newWidth) {
accumulate_max(newHeight, top + height);
}
}
if (!_caption.isEmpty()) {
const auto captionw = newWidth - st::msgPadding.left() - st::msgPadding.right();
newHeight += st::mediaCaptionSkip + _caption.countHeight(captionw);
@ -223,6 +240,7 @@ void GroupedMedia::draw(
: IsGroupItemSelection(selection, i)
? FullSelection
: TextSelection();
const auto last = (i + 1 == count);
part.content->drawGrouped(
p,
clip,
@ -232,7 +250,8 @@ void GroupedMedia::draw(
part.sides,
cornersFromSides(part.sides),
&part.cacheKey,
&part.cache);
&part.cache,
last);
}
// date
@ -262,13 +281,17 @@ void GroupedMedia::draw(
TextState GroupedMedia::getPartState(
QPoint point,
StateRequest request) const {
auto index = 0;
for (const auto &part : _parts) {
++index;
if (part.geometry.contains(point)) {
const auto last = (index == _parts.size());
auto result = part.content->getStateGrouped(
part.geometry,
part.sides,
point,
request);
request,
last);
result.itemId = part.item->fullId();
return result;
}
@ -372,6 +395,9 @@ auto GroupedMedia::getBubbleSelectionIntervals(
last = BubbleSelectionInterval{ newTop, newHeight };
}
}
if (IsGroupItemSelection(selection, _parts.size() - 1)) {
result.back().height = height() - result.back().top;
}
return result;
}
@ -465,6 +491,10 @@ HistoryMessageEdited *GroupedMedia::displayedEditBadge() const {
void GroupedMedia::updateNeedBubbleState() {
const auto captionItem = [&]() -> HistoryItem* {
if (_mode == Mode::Column) {
const auto last = _parts.back().item.get();
return last->emptyText() ? nullptr : last;
}
auto result = (HistoryItem*)nullptr;
for (const auto &part : _parts) {
if (!part.item->emptyText()) {
@ -519,7 +549,7 @@ bool GroupedMedia::needsBubble() const {
}
bool GroupedMedia::computeNeedBubble() const {
if (!_caption.isEmpty() || _mode == Mode::Playlist) {
if (!_caption.isEmpty() || _mode == Mode::Column) {
return true;
}
if (const auto item = _parent->data()) {
@ -537,7 +567,7 @@ bool GroupedMedia::computeNeedBubble() const {
}
bool GroupedMedia::needInfoDisplay() const {
return (_mode != Mode::Playlist)
return (_mode != Mode::Column)
&& (_parent->data()->id < 0
|| _parent->isUnderCursor()
|| _parent->isLastAndSelfMessage());

View file

@ -86,7 +86,7 @@ public:
void updateNeedBubbleState() override;
bool needsBubble() const override;
bool customInfoLayout() const override {
return _caption.isEmpty() && (_mode != Mode::Playlist);
return _caption.isEmpty() && (_mode != Mode::Column);
}
bool allowsFastShare() const override {
return true;
@ -102,7 +102,7 @@ public:
private:
enum class Mode : char {
Grid,
Playlist,
Column,
};
struct Part {
Part(

View file

@ -465,12 +465,16 @@ TextState Photo::textState(QPoint point, StateRequest request) const {
return result;
}
QSize Photo::sizeForGrouping() const {
QSize Photo::sizeForGroupingOptimal(int maxWidth, bool last) const {
const auto width = _data->width();
const auto height = _data->height();
return { std::max(width, 1), std::max(height, 1) };
}
QSize Photo::sizeForGrouping(int width, bool last) const {
return sizeForGroupingOptimal(width, last);
}
void Photo::drawGrouped(
Painter &p,
const QRect &clip,
@ -480,7 +484,8 @@ void Photo::drawGrouped(
RectParts sides,
RectParts corners,
not_null<uint64*> cacheKey,
not_null<QPixmap*> cache) const {
not_null<QPixmap*> cache,
bool last) const {
ensureDataMediaCreated();
_dataMedia->automaticLoad(_realParent->fullId(), _parent->data());
@ -580,7 +585,8 @@ TextState Photo::getStateGrouped(
const QRect &geometry,
RectParts sides,
QPoint point,
StateRequest request) const {
StateRequest request,
bool last) const {
if (!geometry.contains(point)) {
return {};
}

View file

@ -24,7 +24,7 @@ struct Information;
namespace HistoryView {
class Photo : public File {
class Photo final : public File {
public:
Photo(
not_null<Element*> parent,
@ -58,7 +58,8 @@ public:
return _data;
}
QSize sizeForGrouping() const override;
QSize sizeForGroupingOptimal(int maxWidth, bool last) const override;
QSize sizeForGrouping(int width, bool last) const override;
void drawGrouped(
Painter &p,
const QRect &clip,
@ -68,12 +69,14 @@ public:
RectParts sides,
RectParts corners,
not_null<uint64*> cacheKey,
not_null<QPixmap*> cache) const override;
not_null<QPixmap*> cache,
bool last) const override;
TextState getStateGrouped(
const QRect &geometry,
RectParts sides,
QPoint point,
StateRequest request) const override;
StateRequest request,
bool last) const override;
TextWithEntities getCaption() const override {
return _caption.toTextWithEntities();