diff --git a/Telegram/Resources/icons/menu/rich/media_add.svg b/Telegram/Resources/icons/menu/rich/media_add.svg
new file mode 100644
index 0000000000..126d093e33
--- /dev/null
+++ b/Telegram/Resources/icons/menu/rich/media_add.svg
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_box.cpp b/Telegram/SourceFiles/iv/editor/iv_editor_box.cpp
index 975d3f72e6..19f053c26d 100644
--- a/Telegram/SourceFiles/iv/editor/iv_editor_box.cpp
+++ b/Telegram/SourceFiles/iv/editor/iv_editor_box.cpp
@@ -1202,6 +1202,10 @@ void WindowHost::Impl::setupWindow(ShowWindowDescriptor &&descriptor) {
= std::move(descriptor.requestPhotoEditSource),
.replacePhotoWithList
= std::move(descriptor.replacePhotoWithList),
+ .mediaUploadState = std::move(descriptor.mediaUploadState),
+ .cancelMediaUpload = std::move(descriptor.cancelMediaUpload),
+ .addMediaAndGroupWithBlock
+ = std::move(descriptor.addMediaAndGroupWithBlock),
.imeCompositionStarts = window->imeCompositionStarts(),
},
descriptor.peer,
diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_box.h b/Telegram/SourceFiles/iv/editor/iv_editor_box.h
index 19a75fa2e9..0dfead30a5 100644
--- a/Telegram/SourceFiles/iv/editor/iv_editor_box.h
+++ b/Telegram/SourceFiles/iv/editor/iv_editor_box.h
@@ -86,6 +86,10 @@ struct ShowWindowDescriptor {
Fn requestPhotoEditSource;
Fn, Ui::PreparedList, State::ReplaceTarget)>
replacePhotoWithList;
+ Fn mediaUploadState;
+ Fn, uint64 /*mediaId*/)> cancelMediaUpload;
+ Fn, State::BlockPath, QPointer)>
+ addMediaAndGroupWithBlock;
Fn, QPointer, rpl::producer<>)> requestMap;
Fn closed;
Fn showLimitToast;
diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_session.cpp b/Telegram/SourceFiles/iv/editor/iv_editor_session.cpp
index cd59caadbf..4730301a5f 100644
--- a/Telegram/SourceFiles/iv/editor/iv_editor_session.cpp
+++ b/Telegram/SourceFiles/iv/editor/iv_editor_session.cpp
@@ -676,6 +676,8 @@ private:
std::optional insertTarget;
std::vector items;
int nextIndex = 0;
+ std::optional groupAnchor;
+ int insertedTopLevel = 0;
};
struct QueuedPrepare {
@@ -1442,6 +1444,24 @@ private:
std::move(list),
std::move(replaceTarget));
},
+ .mediaUploadState = [session = shared_from_this()](
+ uint64 mediaId) {
+ return session->mediaUploadStateForMedia(mediaId);
+ },
+ .cancelMediaUpload = [session = shared_from_this()](
+ not_null editor,
+ uint64 mediaId) {
+ session->cancelMediaUploadByMediaId(mediaId);
+ },
+ .addMediaAndGroupWithBlock = [session = shared_from_this()](
+ not_null editor,
+ State::BlockPath path,
+ QPointer parent) {
+ session->addMediaAndGroupWithBlock(
+ editor,
+ std::move(path),
+ std::move(parent));
+ },
.requestMap = [session = shared_from_this()](
not_null editor,
QPointer parent,
@@ -1645,13 +1665,102 @@ private:
return QImage();
}
+ [[nodiscard]] MediaUploadState mediaUploadStateForMedia(uint64 mediaId) {
+ for (const auto &attachment : _attachments) {
+ if (mediaIdMatchesAttachment(mediaId, attachment)) {
+ const auto uploading
+ = (attachment.state == AttachmentState::Uploading)
+ || (attachment.state == AttachmentState::Finalizing);
+ return {
+ .uploading = uploading,
+ .progress = attachment.progress,
+ };
+ }
+ }
+ return {};
+ }
+
+ void cancelMediaUploadByMediaId(uint64 mediaId) {
+ for (const auto &attachment : _attachments) {
+ if (mediaIdMatchesAttachment(mediaId, attachment)) {
+ eraseAttachment(attachment.uploadId);
+ return;
+ }
+ }
+ }
+
+ void addMediaAndGroupWithBlock(
+ not_null editor,
+ State::BlockPath anchor,
+ QPointer parent) {
+ if (!parent) {
+ return;
+ }
+ _editor = editor;
+ const auto weak = base::make_weak(this);
+ const auto editorPointer = QPointer(editor.get());
+ auto callback = [weak, editorPointer, anchor = std::move(anchor)](
+ FileDialog::OpenResult &&result) mutable {
+ if (const auto session = weak.get()) {
+ session->applyAddToCollageList(
+ editorPointer,
+ std::move(result),
+ std::move(anchor));
+ }
+ };
+ FileDialog::GetOpenPaths(
+ std::move(parent),
+ tr::lng_choose_files(tr::now),
+ FileDialog::PhotoVideoFilesFilter(),
+ std::move(callback));
+ }
+
+ void applyAddToCollageList(
+ QPointer editor,
+ FileDialog::OpenResult &&result,
+ State::BlockPath anchor) {
+ if (!editor) {
+ return;
+ }
+ auto showError = [=](tr::phrase<> phrase) {
+ showToast(phrase(tr::now));
+ };
+ auto list = Storage::PreparedFileFromFilesDialog(
+ std::move(result),
+ [](const PreparedList &) {
+ return true;
+ },
+ showError,
+ st::sendMediaPreviewSize,
+ _session->premium());
+ if (!list) {
+ return;
+ }
+ const auto selection = _state->preparedSelectionForBlock(anchor);
+ auto target = PreparedMediaPasteTarget{
+ .blockDrop = Markdown::PreparedEditBlockDropTarget{
+ .container = selection.blocks.container,
+ .insertIndex = anchor.index + 1,
+ },
+ };
+ applyPreparedList(
+ editor,
+ std::move(*list),
+ ++_prepareBatchId,
+ AttachmentInsertMode::ClipboardPaste,
+ std::move(target),
+ std::nullopt,
+ anchor);
+ }
+
void applyPreparedList(
QPointer editor,
PreparedList list,
uint64 batchId,
AttachmentInsertMode insertMode = AttachmentInsertMode::Normal,
std::optional insertTarget = std::nullopt,
- std::optional replaceTarget = std::nullopt) {
+ std::optional replaceTarget = std::nullopt,
+ std::optional groupAnchor = std::nullopt) {
const auto effectiveInsertMode = replaceTarget
? AttachmentInsertMode::ReplaceBlock
: insertMode;
@@ -1692,6 +1801,7 @@ private:
.insertMode = effectiveInsertMode,
.insertTarget = insertTarget,
.items = std::vector(totalCount),
+ .groupAnchor = std::move(groupAnchor),
});
}
auto order = 0;
@@ -2844,9 +2954,11 @@ private:
if (uploadIds.empty()) {
return;
}
- if (uploadIds.size() == 1) {
- if (const auto attachment = findAttachment(uploadIds.front())) {
- blocks.push_back(makeAttachmentBlock(*attachment));
+ if (batch->groupAnchor || uploadIds.size() == 1) {
+ for (const auto &uploadId : uploadIds) {
+ if (const auto attachment = findAttachment(uploadId)) {
+ blocks.push_back(makeAttachmentBlock(*attachment));
+ }
}
} else {
blocks.push_back(makeGroupedAttachmentBlock(uploadIds));
@@ -2956,6 +3068,7 @@ private:
batch->nextIndex = cursor;
}
if (blocks.empty()) {
+ groupBatchIntoCollageIfFinished(batchId);
if (eraseFinishedMediaBatch(batchId)) {
maybeContinueDeferredSubmit();
}
@@ -2963,6 +3076,7 @@ private:
}
const auto editor = batch->editor;
_editor = editor;
+ batch->insertedTopLevel += int(blocks.size());
if (batch->insertMode == AttachmentInsertMode::ClipboardPaste
&& batch->insertTarget) {
auto target = std::move(*batch->insertTarget);
@@ -2996,11 +3110,33 @@ private:
}
}
requestEditorUpdate();
+ groupBatchIntoCollageIfFinished(batchId);
if (eraseFinishedMediaBatch(batchId)) {
maybeContinueDeferredSubmit();
}
}
+ void groupBatchIntoCollageIfFinished(uint64 batchId) {
+ const auto batch = findMediaBatch(batchId);
+ if (!batch || !batch->groupAnchor || !batch->editor) {
+ return;
+ }
+ const auto finished = std::all_of(
+ batch->items.begin(),
+ batch->items.end(),
+ [](const MediaBatchItem &item) {
+ return (item.state == MediaBatchItemState::Inserted)
+ || (item.state == MediaBatchItemState::Skipped);
+ });
+ if (!finished) {
+ return;
+ }
+ const auto anchor = *batch->groupAnchor;
+ const auto insertedCount = batch->insertedTopLevel;
+ batch->groupAnchor = std::nullopt;
+ batch->editor->groupBlocksIntoCollage(anchor, insertedCount);
+ }
+
[[nodiscard]] bool mediaIdMatchesAttachment(
uint64 id,
const AttachmentRecord &attachment) const {
diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_state.h b/Telegram/SourceFiles/iv/editor/iv_editor_state.h
index 9ddb0cc0bd..60c43bb771 100644
--- a/Telegram/SourceFiles/iv/editor/iv_editor_state.h
+++ b/Telegram/SourceFiles/iv/editor/iv_editor_state.h
@@ -488,6 +488,8 @@ public:
[[nodiscard]] bool setGroupedMediaIntent(
const BlockPath &path,
RichPage::GroupedMediaIntent intent);
+ [[nodiscard]] Markdown::PreparedEditSelection preparedSelectionForBlock(
+ const BlockPath &path) const;
private:
struct StructuralBlockRange {
@@ -826,8 +828,6 @@ private:
const BlockPath &path,
int itemIndex,
std::vector *steps) const;
- [[nodiscard]] Markdown::PreparedEditSelection preparedSelectionForBlock(
- const BlockPath &path) const;
[[nodiscard]] Markdown::PreparedEditSelection preparedSelectionForListItem(
const BlockPath &path,
int itemIndex) const;
@@ -894,6 +894,11 @@ enum class RequestMediaType : uchar {
PhotoVideoAudio,
};
+struct MediaUploadState {
+ bool uploading = false;
+ float64 progress = 0.;
+};
+
[[nodiscard]] bool CanEditRichPage(const RichPage &page);
[[nodiscard]] bool CanEditRichPage(
const std::shared_ptr &page);
diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp b/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp
index 9763492167..c17c4efaaf 100644
--- a/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp
+++ b/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp
@@ -36,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/chat/chat_style.h"
#include "ui/chat/chat_theme.h"
#include "ui/click_handler.h"
+#include "ui/effects/radial_animation.h"
#include "ui/image/image.h"
#include "ui/image/image_location.h"
#include "ui/layers/generic_box.h"
@@ -57,6 +58,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/palette.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
+#include "styles/style_chat_helpers.h"
#include "styles/style_iv.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
@@ -1102,6 +1104,18 @@ template
}
}
+[[nodiscard]] uint64 MediaIdForBlock(const RichPage::Block &block) {
+ switch (block.kind) {
+ case RichPage::BlockKind::Photo:
+ return block.photoId;
+ case RichPage::BlockKind::Video:
+ case RichPage::BlockKind::Audio:
+ return block.documentId;
+ default:
+ return uint64(0);
+ }
+}
+
[[nodiscard]] bool IsPhotoVideoBlockKind(RichPage::BlockKind kind) {
return (kind == RichPage::BlockKind::Photo)
|| (kind == RichPage::BlockKind::Video);
@@ -2596,6 +2610,9 @@ Widget::Widget(
, _applyPreparedMedia(std::move(services.applyPreparedMedia))
, _requestPhotoEditSource(std::move(services.requestPhotoEditSource))
, _replacePhotoWithList(std::move(services.replacePhotoWithList))
+, _mediaUploadState(std::move(services.mediaUploadState))
+, _cancelMediaUpload(std::move(services.cancelMediaUpload))
+, _addMediaAndGroupWithBlock(std::move(services.addMediaAndGroupWithBlock))
, _peer(peer)
, _state(std::move(state))
, _showLimitToast(std::move(showLimitToast))
@@ -5838,6 +5855,174 @@ void Widget::editPhotoBlock(State::BlockPath path) {
_show->showLayer(std::move(layer), Ui::LayerOption::KeepOther);
}
+MediaUploadState Widget::mediaUploadStateForBlock(
+ const State::BlockPath &path) const {
+ const auto block = BlockFromPath(_state->richPage(), path);
+ if (!block) {
+ return {};
+ }
+ const auto mediaId = MediaIdForBlock(*block);
+ return _mediaUploadState ? _mediaUploadState(mediaId) : MediaUploadState();
+}
+
+Widget::MediaControlLayout Widget::mediaControlLayout(
+ QRect mediaRect) const {
+ const auto d = st::ivEditorMediaCornerSize;
+ const auto skip = st::ivEditorMediaCornerSkip;
+ const auto &r = mediaRect;
+ const auto threeDots = QRect(r.left() + skip, r.top() + skip, d, d);
+ const auto plus = QRect(r.right() - skip - d + 1, r.top() + skip, d, d);
+ const auto rs = st::ivEditorMediaUploadRadialSize;
+ const auto radial = QRect(
+ r.center().x() - rs / 2,
+ r.center().y() - rs / 2,
+ rs,
+ rs);
+ return { threeDots, plus, radial };
+}
+
+void Widget::paintMediaControls(Painter &p, QPoint topLeft) {
+ auto anyUploading = false;
+ for (const auto &geo : _article->mediaBlockGeometries()) {
+ if (geo.grouped || geo.visibleMediaRect.isEmpty()) {
+ continue;
+ }
+ const auto path = _state->convertBlockPath(geo.block);
+ if (!path) {
+ continue;
+ }
+ const auto block = BlockFromPath(_state->richPage(), *path);
+ if (!block || !IsSimpleMediaBlockKind(block->kind)) {
+ continue;
+ }
+ const auto layout = mediaControlLayout(geo.visibleMediaRect);
+ const auto uploadState = mediaUploadStateForBlock(*path);
+ if (uploadState.uploading) {
+ anyUploading = true;
+ if (!_mediaUploadRadial) {
+ _mediaUploadRadial = std::make_unique(
+ [=] { update(); });
+ }
+ if (!_mediaUploadRadial->animating()) {
+ _mediaUploadRadial->start(uploadState.progress);
+ } else {
+ _mediaUploadRadial->update(
+ uploadState.progress,
+ false,
+ crl::now());
+ }
+ const auto radial = layout.radial.translated(topLeft);
+ auto hq = PainterHighQualityEnabler(p);
+ p.setPen(Qt::NoPen);
+ p.setBrush(st::roundedBg);
+ p.drawEllipse(radial);
+ _mediaUploadRadial->draw(
+ p,
+ QRectF(radial),
+ st::ivEditorMediaUploadRadialWidth,
+ st::roundedFg);
+ } else {
+ const auto threeDots = layout.threeDots.translated(topLeft);
+ const auto plus = layout.plus.translated(topLeft);
+ auto hq = PainterHighQualityEnabler(p);
+ p.setPen(Qt::NoPen);
+ p.setBrush(st::roundedBg);
+ p.drawEllipse(threeDots);
+ p.drawEllipse(plus);
+ st::sendBoxAlbumButtonMediaMore.paintInCenter(p, threeDots);
+ st::ivEditorMediaAddIcon.paintInCenter(p, plus);
+ }
+ }
+ if (!anyUploading && _mediaUploadRadial && _mediaUploadRadial->animating()) {
+ _mediaUploadRadial->stop();
+ }
+}
+
+Widget::PressedMediaControl Widget::mediaControlHitTest(
+ QPoint articlePoint) const {
+ for (const auto &geo : _article->mediaBlockGeometries()) {
+ if (geo.grouped || geo.visibleMediaRect.isEmpty()) {
+ continue;
+ }
+ const auto path = _state->convertBlockPath(geo.block);
+ if (!path) {
+ continue;
+ }
+ const auto block = BlockFromPath(_state->richPage(), *path);
+ if (!block || !IsSimpleMediaBlockKind(block->kind)) {
+ continue;
+ }
+ const auto layout = mediaControlLayout(geo.visibleMediaRect);
+ if (mediaUploadStateForBlock(*path).uploading) {
+ if (layout.radial.contains(articlePoint)) {
+ return { MediaControl::UploadRadial, *path };
+ }
+ } else if (layout.threeDots.contains(articlePoint)) {
+ return { MediaControl::ThreeDots, *path };
+ } else if (layout.plus.contains(articlePoint)) {
+ return { MediaControl::Plus, *path };
+ } else if ((block->kind == RichPage::BlockKind::Photo)
+ && geo.visibleMediaRect.contains(articlePoint)) {
+ return { MediaControl::MediaPixels, *path };
+ }
+ }
+ return {};
+}
+
+void Widget::addToCollageFromBlock(const State::BlockPath &path) {
+ if (_addMediaAndGroupWithBlock) {
+ _addMediaAndGroupWithBlock(
+ this,
+ path,
+ QPointer(_outer.get()));
+ }
+}
+
+void Widget::groupBlocksIntoCollage(
+ State::BlockPath anchor,
+ int insertedCount) {
+ if (insertedCount < 1) {
+ return;
+ }
+ auto selection = _state->preparedSelectionForBlock(anchor);
+ selection.blocks.till += insertedCount;
+ if (!_state->canGroupPhotoVideoBlocks(selection)) {
+ return;
+ }
+ [[maybe_unused]] const auto changed = applyMediaBlockChange([&] {
+ return _state->groupPhotoVideoBlocks(
+ selection,
+ RichPage::GroupedMediaIntent::Collage);
+ });
+}
+
+void Widget::cancelMediaUploadForBlock(const State::BlockPath &path) {
+ const auto block = BlockFromPath(_state->richPage(), path);
+ if (!block) {
+ return;
+ }
+ const auto mediaId = MediaIdForBlock(*block);
+ if (_cancelMediaUpload) {
+ _cancelMediaUpload(this, mediaId);
+ }
+ auto target = std::optional();
+ const auto changed = applyMediaBlockChange([=, &target] {
+ const auto current = BlockFromPath(_state->richPage(), path);
+ if (!current || !IsSimpleMediaBlockKind(current->kind)) {
+ return false;
+ }
+ target = _state->removeBlock(path, true);
+ return true;
+ });
+ if (!changed) {
+ return;
+ } else if (target) {
+ activateTextOrdinal(*target, 0);
+ } else {
+ activateInitialNode();
+ }
+}
+
void Widget::touchEvent(QTouchEvent *e) {
if (e->type() == QEvent::TouchCancel) {
_pendingTouchHorizontalScrollPoint = std::nullopt;
@@ -6032,6 +6217,8 @@ void Widget::mousePressEvent(QMouseEvent *e) {
_selectScroll.cancel();
_pressedControl = {};
_pressedControlPoint = std::nullopt;
+ _pressedMediaControl = {};
+ _pressedMediaControlPoint = std::nullopt;
auto articlePoint = e->pos() - articleTopLeft();
const auto horizontalScrollHit = _article->horizontalScrollHit(
articlePoint);
@@ -6049,6 +6236,13 @@ void Widget::mousePressEvent(QMouseEvent *e) {
e->accept();
return;
}
+ const auto mediaControl = mediaControlHitTest(articlePoint);
+ if (mediaControl.valid()) {
+ _pressedMediaControl = mediaControl;
+ _pressedMediaControlPoint = articlePoint;
+ e->accept();
+ return;
+ }
auto hit = _article->hitTest(
articlePoint,
Ui::Text::StateRequest::Flag::LookupSymbol);
@@ -6233,6 +6427,38 @@ void Widget::mouseReleaseEvent(QMouseEvent *e) {
e->accept();
return;
}
+ if (_pressedMediaControl.valid()) {
+ const auto pressed = _pressedMediaControl;
+ const auto pressedPoint = _pressedMediaControlPoint;
+ _pressedMediaControl = {};
+ _pressedMediaControlPoint = std::nullopt;
+ const auto current = mediaControlHitTest(articlePoint);
+ const auto matched = pressedPoint
+ && ((articlePoint - *pressedPoint).manhattanLength()
+ < QApplication::startDragDistance())
+ && (current.control == pressed.control)
+ && (current.path == pressed.path);
+ if (matched) {
+ switch (pressed.control) {
+ case MediaControl::ThreeDots:
+ showSimpleMediaMenu(pressed.path, e->globalPos());
+ break;
+ case MediaControl::Plus:
+ addToCollageFromBlock(pressed.path);
+ break;
+ case MediaControl::MediaPixels:
+ editPhotoBlock(pressed.path);
+ break;
+ case MediaControl::UploadRadial:
+ cancelMediaUploadForBlock(pressed.path);
+ break;
+ case MediaControl::None:
+ break;
+ }
+ }
+ e->accept();
+ return;
+ }
const auto hit = _article->hitTest(
articlePoint,
Ui::Text::StateRequest::Flag::LookupSymbol);
@@ -6491,6 +6717,7 @@ void Widget::paintEvent(QPaintEvent *e) {
p,
textPaintContext(e->rect().translated(-topLeft.x(), -topLeft.y())));
p.restore();
+ paintMediaControls(p, topLeft);
if (!_articleSelectionDrag.indicatorRect.isEmpty()) {
auto color = st::windowActiveTextFg->c;
color.setAlphaF(color.alphaF() * 0.7);
diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_widget.h b/Telegram/SourceFiles/iv/editor/iv_editor_widget.h
index 4af8f809a6..63b851c2c9 100644
--- a/Telegram/SourceFiles/iv/editor/iv_editor_widget.h
+++ b/Telegram/SourceFiles/iv/editor/iv_editor_widget.h
@@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include
#include
+class Painter;
class QEvent;
class QContextMenuEvent;
class QInputMethodEvent;
@@ -42,6 +43,7 @@ class ChatTheme;
class InputField;
class PopupMenu;
class ElasticScroll;
+class RadialAnimation;
struct PreparedList;
} // namespace Ui
@@ -84,6 +86,10 @@ struct WidgetServices {
Fn requestPhotoEditSource;
Fn, Ui::PreparedList, State::ReplaceTarget)>
replacePhotoWithList;
+ Fn mediaUploadState;
+ Fn, uint64 /*mediaId*/)> cancelMediaUpload;
+ Fn, State::BlockPath, QPointer)>
+ addMediaAndGroupWithBlock;
rpl::producer<> imeCompositionStarts;
};
@@ -120,6 +126,7 @@ public:
void pastePreparedBlocks(
std::vector blocks,
PreparedMediaPasteTarget target);
+ void groupBlocksIntoCollage(State::BlockPath anchor, int insertedCount);
void insertHeading1();
void insertBlockquote();
void insertEmoji(EmojiPtr emoji);
@@ -238,6 +245,22 @@ protected:
void requestRelayout(QRect articleRect) override;
private:
+ enum class MediaControl : uchar {
+ None,
+ ThreeDots,
+ Plus,
+ MediaPixels,
+ UploadRadial,
+ };
+ struct PressedMediaControl {
+ MediaControl control = MediaControl::None;
+ State::BlockPath path;
+
+ [[nodiscard]] bool valid() const {
+ return control != MediaControl::None;
+ }
+ };
+
struct InlineFieldStyleData {
const style::TextStyle *textStyle = nullptr;
int lineHeight = 0;
@@ -743,6 +766,20 @@ private:
[[nodiscard]] bool applyMediaBlockChange(Fn change);
void requestReplaceMedia(State::BlockPath path);
void editPhotoBlock(State::BlockPath path);
+ void paintMediaControls(Painter &p, QPoint topLeft);
+ struct MediaControlLayout {
+ QRect threeDots;
+ QRect plus;
+ QRect radial;
+ };
+ [[nodiscard]] MediaControlLayout mediaControlLayout(
+ QRect mediaRect) const;
+ [[nodiscard]] PressedMediaControl mediaControlHitTest(
+ QPoint articlePoint) const;
+ void cancelMediaUploadForBlock(const State::BlockPath &path);
+ void addToCollageFromBlock(const State::BlockPath &path);
+ [[nodiscard]] MediaUploadState mediaUploadStateForBlock(
+ const State::BlockPath &path) const;
[[nodiscard]] Markdown::PreparedEditSelection structuralSelectionFromHits(
const Markdown::PreparedEditHit &anchor,
const Markdown::PreparedEditHit &focus) const;
@@ -771,6 +808,10 @@ private:
const Fn _requestPhotoEditSource;
const Fn, Ui::PreparedList, State::ReplaceTarget)>
_replacePhotoWithList;
+ const Fn _mediaUploadState;
+ const Fn, uint64)> _cancelMediaUpload;
+ const Fn, State::BlockPath, QPointer)>
+ _addMediaAndGroupWithBlock;
const not_null _peer;
const std::shared_ptr _state;
const Fn _showLimitToast;
@@ -828,6 +869,9 @@ private:
bool _keyboardStructuralSelectionActive = false;
Markdown::MarkdownArticleEditControlHit _pressedControl;
std::optional _pressedControlPoint;
+ PressedMediaControl _pressedMediaControl;
+ std::optional _pressedMediaControlPoint;
+ std::unique_ptr _mediaUploadRadial;
HorizontalScrollDrag _horizontalScrollDrag = HorizontalScrollDrag::None;
std::optional _pendingTouchHorizontalScrollPoint;
bool _syncingInlineFieldGeometry = false;
diff --git a/Telegram/SourceFiles/iv/iv.style b/Telegram/SourceFiles/iv/iv.style
index 73f8bcc0bf..f6563b14ea 100644
--- a/Telegram/SourceFiles/iv/iv.style
+++ b/Telegram/SourceFiles/iv/iv.style
@@ -195,6 +195,11 @@ ivEditorToolbarAttachIcon: icon {{ "menu/rich/attach-24x24", menuIconColor }};
ivEditorToolbarDetailsIcon: icon {{ "menu/rich/details-24x24", menuIconColor }};
ivEditorToolbarTableIcon: icon {{ "menu/rich/table-24x24", menuIconColor }};
ivEditorToolbarLocationIcon: icon {{ "menu/rich/location-24x24", menuIconColor }};
+ivEditorMediaCornerSize: 27px;
+ivEditorMediaCornerSkip: 6px;
+ivEditorMediaAddIcon: icon {{ "menu/rich/media_add-20x20", windowFgActive }};
+ivEditorMediaUploadRadialSize: 44px;
+ivEditorMediaUploadRadialWidth: 4px;
ivEditorTableAddRowAboveIcon: icon {{ "menu/rich/table_add_row_above-24x24", menuIconColor }};
ivEditorTableAddRowBelowIcon: icon {{ "menu/rich/table_add_row_below-24x24", menuIconColor }};
ivEditorTableAddColumnLeftIcon: icon {{ "menu/rich/table_add_column_left-24x24", menuIconColor }};
diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp
index 2044dfbc13..23c618bc15 100644
--- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp
+++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp
@@ -1843,6 +1843,29 @@ void ApplyOwnerContentGeometry(
return fallback;
}
+void CollectMediaBlockGeometries(
+ std::vector *out,
+ const std::vector &blocks) {
+ for (const auto &block : blocks) {
+ const auto media = (block.kind == PreparedBlockKind::Photo)
+ || (block.kind == PreparedBlockKind::Video)
+ || (block.kind == PreparedBlockKind::Audio)
+ || (block.kind == PreparedBlockKind::Map)
+ || (block.kind == PreparedBlockKind::GroupedMedia);
+ if (media && block.editBlock) {
+ out->push_back({
+ .block = *block.editBlock,
+ .mediaRect = block.mediaRect,
+ .visibleMediaRect = block.visibleMediaRect,
+ .grouped = (block.kind == PreparedBlockKind::GroupedMedia),
+ });
+ }
+ if (!block.children.empty()) {
+ CollectMediaBlockGeometries(out, block.children);
+ }
+ }
+}
+
[[nodiscard]] PreparedEditHit EditFallbackHitForBlock(
const LaidOutBlock &block) {
if (block.editListItem) {
@@ -3259,6 +3282,8 @@ public:
Ui::Text::StateRequest::Flags flags) const;
[[nodiscard]] PreparedEditHit editHitTest(QPoint point) const;
+ [[nodiscard]] std::vector
+ mediaBlockGeometries() const;
[[nodiscard]] MarkdownArticleDropLocation editDropTarget(
QPoint point) const;
[[nodiscard]] MarkdownArticleDropLocation editStructuralDropTarget(
@@ -3949,6 +3974,13 @@ PreparedEditHit MarkdownArticle::Impl::editHitTest(QPoint point) const {
return EditHitForBlocks(_blocks, point);
}
+std::vector
+MarkdownArticle::Impl::mediaBlockGeometries() const {
+ auto result = std::vector();
+ CollectMediaBlockGeometries(&result, _blocks);
+ return result;
+}
+
MarkdownArticleDropLocation MarkdownArticle::Impl::editDropTarget(
QPoint point) const {
if (const auto result = hitTest(
@@ -5881,6 +5913,11 @@ QRect MarkdownArticle::segmentRect(int segmentIndex) const {
return _impl->segmentRect(segmentIndex);
}
+std::vector
+MarkdownArticle::mediaBlockGeometries() const {
+ return _impl->mediaBlockGeometries();
+}
+
QRect MarkdownArticle::displayMathEditRect(int segmentIndex) const {
return _impl->displayMathEditRect(segmentIndex);
}
diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h
index 1195626400..56089db194 100644
--- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h
+++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h
@@ -301,6 +301,13 @@ struct MarkdownArticleAnchorExpansion {
bool changed = false;
};
+struct MarkdownArticleMediaGeometry {
+ PreparedEditBlockSource block;
+ QRect mediaRect;
+ QRect visibleMediaRect;
+ bool grouped = false;
+};
+
class MarkdownArticle {
public:
MarkdownArticle(
@@ -386,6 +393,8 @@ public:
[[nodiscard]] QRect textSegmentRect(int segmentIndex) const;
[[nodiscard]] QRect logicalSegmentRect(int segmentIndex) const;
[[nodiscard]] QRect segmentRect(int segmentIndex) const;
+ [[nodiscard]] std::vector
+ mediaBlockGeometries() const;
[[nodiscard]] QRect displayMathEditRect(int segmentIndex) const;
[[nodiscard]] QRect displayMathBlockRect(int segmentIndex) const;
[[nodiscard]] int pullquoteAvailableTextWidthForEditableLeaf(