Improve embeds usability.

This commit is contained in:
John Preston
2026-05-18 12:47:54 +04:00
parent 2c8ba4e694
commit 05e026c38c
18 changed files with 1120 additions and 214 deletions
+1
View File
@@ -7888,6 +7888,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_contact_send_message" = "Message";
"lng_iv_open_in_browser" = "Open in Browser";
"lng_iv_click_to_view" = "Click to View";
"lng_iv_share" = "Share";
"lng_iv_join_channel" = "Join";
"lng_iv_window_title" = "Instant View";
+9 -1
View File
@@ -166,6 +166,10 @@ MarkdownEmbedPost {
MarkdownPlaceholder {
padding: margins;
minHeight: pixels;
border: pixels;
radius: pixels;
spinnerWidth: pixels;
spinnerSize: pixels;
captionSkip: pixels;
labelStyle: TextStyle;
}
@@ -417,8 +421,12 @@ defaultMarkdownEmbedPost: MarkdownEmbedPost {
captionSkip: 12px;
}
defaultMarkdownPlaceholder: MarkdownPlaceholder {
padding: margins(16px, 12px, 16px, 12px);
padding: margins(16px, 24px, 16px, 24px);
minHeight: 48px;
border: 4px;
radius: 16px;
spinnerWidth: 4px;
spinnerSize: 24px;
captionSkip: 14px;
labelStyle: TextStyle(defaultMarkdownBodyStyle) {
font: font(16px semibold);
@@ -16,10 +16,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/dynamic_image.h"
#include "styles/style_iv.h"
#include "styles/style_widgets.h"
#include <algorithm>
#include <limits>
#include <unordered_map>
#include <unordered_set>
#include <utility>
namespace Iv::Markdown {
@@ -109,6 +111,54 @@ void RestoreRelatedArticleThumbnailStates(
}
}
void CollectPlaceholderIds(
const std::vector<LaidOutBlock> &blocks,
std::unordered_set<uint64> *result) {
if (!result) {
return;
}
for (const auto &block : blocks) {
if (block.placeholderId) {
result->emplace(block.placeholderId.value);
}
CollectPlaceholderIds(block.children, result);
}
}
[[nodiscard]] LaidOutBlock *FindPlaceholderBlock(
std::vector<LaidOutBlock> *blocks,
PreparedPlaceholderBlockId id) {
if (!blocks || !id) {
return nullptr;
}
for (auto &block : *blocks) {
if (block.placeholderId.value == id.value) {
return &block;
}
if (const auto child = FindPlaceholderBlock(&block.children, id)) {
return child;
}
}
return nullptr;
}
[[nodiscard]] const LaidOutBlock *FindPlaceholderBlock(
const std::vector<LaidOutBlock> &blocks,
PreparedPlaceholderBlockId id) {
if (!id) {
return nullptr;
}
for (const auto &block : blocks) {
if (block.placeholderId.value == id.value) {
return &block;
}
if (const auto child = FindPlaceholderBlock(block.children, id)) {
return child;
}
}
return nullptr;
}
[[nodiscard]] PendingHighlightKey PendingHighlightKeyForBlock(
const LaidOutBlock &block) {
return {
@@ -310,6 +360,12 @@ void RebuildVisibleSegmentLookup(
}
} else {
applyActivation(segment.block->activation);
if (result.mediaActivation.kind == MediaActivationKind::Embed
&& segment.block->placeholderRuntime) {
result.state.link = segment.block->placeholderRuntime->clickHandler;
result.placeholderLocalPoint = point
- segment.block->mediaRect.topLeft();
}
}
}
result.direct = true;
@@ -377,6 +433,18 @@ void ClearColorizedFormulaImages(std::vector<LaidOutBlock> *blocks) {
} // namespace
PlaceholderBlockRuntime::PlaceholderBlockRuntime(Fn<void()> repaint)
: clickHandler(std::make_shared<LambdaClickHandler>([] {
}))
, loadingAnimation(
[repaint = std::move(repaint)] {
if (repaint) {
repaint();
}
},
st::defaultInfiniteRadialAnimation) {
}
class MarkdownArticle::Impl final : public CodeBlockSyntaxHighlightTracker {
public:
explicit Impl(std::shared_ptr<MathRenderer> renderer);
@@ -446,6 +514,12 @@ public:
[[nodiscard]] MediaBlockHost *mediaBlockHost() const;
void setPlaceholderLoading(PreparedPlaceholderBlockId id);
void clearPlaceholderLoading(PreparedPlaceholderBlockId id);
void clearAllPlaceholderLoading();
void addPlaceholderRipple(PreparedPlaceholderBlockId id, QPoint point);
void stopPlaceholderRipple(PreparedPlaceholderBlockId id);
void invalidateLayout();
private:
@@ -459,6 +533,15 @@ private:
void refreshMediaBlockHosts();
void clearPlaceholderRuntimes();
[[nodiscard]] std::shared_ptr<PlaceholderBlockRuntime>
getOrCreatePlaceholderRuntime(PreparedPlaceholderBlockId id);
void prunePlaceholderRuntimes();
void requestPlaceholderRepaint(PreparedPlaceholderBlockId id);
[[nodiscard]] std::shared_ptr<MediaBlock> getOrCreateMediaBlock(
const PreparedBlock &prepared);
@@ -486,6 +569,10 @@ private:
void resetFormulaRasterCache();
void setPlaceholderLoadingValue(
PreparedPlaceholderBlockId id,
bool loading);
void relayout(int width);
mutable MarkdownArticleContent _content;
@@ -499,6 +586,8 @@ private:
int _height = 0;
std::vector<LaidOutBlock> _blocks;
std::unordered_map<uint64, std::shared_ptr<MediaBlock>> _mediaBlocks;
std::unordered_map<uint64, std::shared_ptr<PlaceholderBlockRuntime>>
_placeholderRuntimes;
std::unordered_map<
uint64,
RelatedArticleThumbnailState> _relatedArticleThumbnails;
@@ -544,6 +633,7 @@ void MarkdownArticle::Impl::setTextRepaintCallbacks(
void MarkdownArticle::Impl::setContent(MarkdownArticleContent content) {
clearMediaBlocks();
clearPlaceholderRuntimes();
_relatedArticleThumbnails.clear();
_content = std::move(content);
ClearInlineFormulaObjectCache(_inlineFormulaObjects);
@@ -761,6 +851,79 @@ MediaBlockHost *MarkdownArticle::Impl::mediaBlockHost() const {
return _mediaBlockHost;
}
void MarkdownArticle::Impl::setPlaceholderLoading(
PreparedPlaceholderBlockId id) {
setPlaceholderLoadingValue(id, true);
}
void MarkdownArticle::Impl::clearPlaceholderLoading(
PreparedPlaceholderBlockId id) {
setPlaceholderLoadingValue(id, false);
}
void MarkdownArticle::Impl::clearAllPlaceholderLoading() {
auto repaintIds = std::vector<PreparedPlaceholderBlockId>();
repaintIds.reserve(_placeholderRuntimes.size());
for (const auto &[value, runtime] : _placeholderRuntimes) {
if (!runtime || !runtime->loading) {
continue;
}
runtime->loading = false;
runtime->loadingAnimation.stop(anim::type::instant);
repaintIds.push_back({ .value = value });
}
for (const auto id : repaintIds) {
requestPlaceholderRepaint(id);
}
}
void MarkdownArticle::Impl::addPlaceholderRipple(
PreparedPlaceholderBlockId id,
QPoint point) {
const auto block = FindPlaceholderBlock(&_blocks, id);
if (!block) {
return;
}
auto runtime = block->placeholderRuntime
? block->placeholderRuntime
: getOrCreatePlaceholderRuntime(id);
if (!runtime) {
return;
}
block->placeholderRuntime = runtime;
const auto size = block->mediaRect.size();
if (!runtime->ripple || runtime->rippleSize != size) {
runtime->ripple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
Ui::RippleAnimation::RoundRectMask(
size,
st::defaultMarkdown.placeholder.radius),
[=] {
requestPlaceholderRepaint(id);
});
runtime->rippleSize = size;
}
point.setX(std::clamp(point.x(), 0, std::max(size.width() - 1, 0)));
point.setY(std::clamp(point.y(), 0, std::max(size.height() - 1, 0)));
runtime->ripple->add(point);
requestPlaceholderRepaint(id);
}
void MarkdownArticle::Impl::stopPlaceholderRipple(
PreparedPlaceholderBlockId id) {
if (!id) {
return;
}
const auto i = _placeholderRuntimes.find(id.value);
if (i == end(_placeholderRuntimes)
|| !i->second
|| !i->second->ripple) {
return;
}
i->second->ripple->lastStop();
requestPlaceholderRepaint(id);
}
void MarkdownArticle::Impl::invalidateLayout() {
_width = -1;
_height = 0;
@@ -803,6 +966,10 @@ void MarkdownArticle::Impl::clearMediaBlocks() {
_mediaBlocks.clear();
}
void MarkdownArticle::Impl::clearPlaceholderRuntimes() {
_placeholderRuntimes.clear();
}
void MarkdownArticle::Impl::refreshMediaBlockHosts() {
for (const auto &[id, block] : _mediaBlocks) {
if (block) {
@@ -811,6 +978,48 @@ void MarkdownArticle::Impl::refreshMediaBlockHosts() {
}
}
std::shared_ptr<PlaceholderBlockRuntime>
MarkdownArticle::Impl::getOrCreatePlaceholderRuntime(
PreparedPlaceholderBlockId id) {
if (!id) {
return nullptr;
}
if (const auto i = _placeholderRuntimes.find(id.value);
i != end(_placeholderRuntimes)) {
return i->second;
}
auto runtime = std::make_shared<PlaceholderBlockRuntime>([=] {
requestPlaceholderRepaint(id);
});
_placeholderRuntimes.emplace(id.value, runtime);
return runtime;
}
void MarkdownArticle::Impl::prunePlaceholderRuntimes() {
auto live = std::unordered_set<uint64>();
CollectPlaceholderIds(_blocks, &live);
for (auto i = _placeholderRuntimes.begin(); i != _placeholderRuntimes.end();) {
if (live.find(i->first) != end(live)) {
++i;
} else {
i = _placeholderRuntimes.erase(i);
}
}
}
void MarkdownArticle::Impl::requestPlaceholderRepaint(
PreparedPlaceholderBlockId id) {
if (const auto block = FindPlaceholderBlock(_blocks, id)) {
if (_textRepaintRect) {
_textRepaintRect(block->mediaRect);
} else if (_textRepaint) {
_textRepaint();
}
} else if (_textRepaint) {
_textRepaint();
}
}
std::shared_ptr<MediaBlock> MarkdownArticle::Impl::getOrCreateMediaBlock(
const PreparedBlock &prepared) {
switch (prepared.kind) {
@@ -955,6 +1164,33 @@ void MarkdownArticle::Impl::resetFormulaRasterCache() {
_formulaRenders.resize(_content.formulas.size());
}
void MarkdownArticle::Impl::setPlaceholderLoadingValue(
PreparedPlaceholderBlockId id,
bool loading) {
if (!id) {
return;
}
const auto runtime = loading
? getOrCreatePlaceholderRuntime(id)
: [&]() -> std::shared_ptr<PlaceholderBlockRuntime> {
if (const auto i = _placeholderRuntimes.find(id.value);
i != end(_placeholderRuntimes)) {
return i->second;
}
return nullptr;
}();
if (!runtime || runtime->loading == loading) {
return;
}
runtime->loading = loading;
if (loading) {
runtime->loadingAnimation.start();
} else {
runtime->loadingAnimation.stop(anim::type::instant);
}
requestPlaceholderRepaint(id);
}
void MarkdownArticle::Impl::relayout(int width) {
width = std::max(width, 1);
if (_width == width) {
@@ -987,6 +1223,9 @@ void MarkdownArticle::Impl::relayout(int width) {
context.mediaBlockFactory = [=](const PreparedBlock &prepared) {
return getOrCreateMediaBlock(prepared);
};
context.placeholderRuntimeFactory = [=](PreparedPlaceholderBlockId id) {
return getOrCreatePlaceholderRuntime(id);
};
const auto y = LayoutBlocks(
_content.blocks.blocks,
&_content.formulas,
@@ -999,7 +1238,8 @@ void MarkdownArticle::Impl::relayout(int width) {
page.left(),
page.top(),
innerWidth,
std::move(context));
context);
prunePlaceholderRuntimes();
RestoreRelatedArticleThumbnailStates(
&_blocks,
_relatedArticleThumbnails);
@@ -1138,4 +1378,26 @@ MediaBlockHost *MarkdownArticle::mediaBlockHost() const {
return _impl->mediaBlockHost();
}
void MarkdownArticle::setPlaceholderLoading(PreparedPlaceholderBlockId id) {
_impl->setPlaceholderLoading(id);
}
void MarkdownArticle::clearPlaceholderLoading(PreparedPlaceholderBlockId id) {
_impl->clearPlaceholderLoading(id);
}
void MarkdownArticle::clearAllPlaceholderLoading() {
_impl->clearAllPlaceholderLoading();
}
void MarkdownArticle::addPlaceholderRipple(
PreparedPlaceholderBlockId id,
QPoint point) {
_impl->addPlaceholderRipple(id, point);
}
void MarkdownArticle::stopPlaceholderRipple(PreparedPlaceholderBlockId id) {
_impl->stopPlaceholderRipple(id);
}
} // namespace Iv::Markdown
@@ -12,6 +12,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "iv/markdown/iv_markdown_prepare.h"
#include "spellcheck/spellcheck_highlight_syntax.h"
#include "ui/click_handler.h"
#include "ui/effects/radial_animation.h"
#include "ui/effects/ripple_animation.h"
#include "ui/painter.h"
#include "ui/text/text.h"
@@ -21,6 +24,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Iv::Markdown {
struct PlaceholderBlockRuntime {
explicit PlaceholderBlockRuntime(Fn<void()> repaint);
ClickHandlerPtr clickHandler;
bool loading = false;
Ui::InfiniteRadialAnimation loadingAnimation;
std::unique_ptr<Ui::RippleAnimation> ripple;
QSize rippleSize;
};
struct MarkdownArticlePaintCaches {
Ui::Text::QuotePaintCache *pre = nullptr;
Ui::Text::QuotePaintCache *blockquote = nullptr;
@@ -35,6 +48,7 @@ struct MarkdownArticleHitTestResult {
Ui::Text::StateResult state;
std::optional<PreparedLink> preparedLink;
MediaActivation mediaActivation;
QPoint placeholderLocalPoint;
int forcedOffset = -1;
bool direct = false;
@@ -155,11 +169,17 @@ public:
void invalidatePaletteCache();
void invalidateRasterCache();
[[nodiscard]] MediaBlockHost *mediaBlockHost() const;
void setPlaceholderLoading(PreparedPlaceholderBlockId id);
void clearPlaceholderLoading(PreparedPlaceholderBlockId id);
void clearAllPlaceholderLoading();
void addPlaceholderRipple(PreparedPlaceholderBlockId id, QPoint point);
void stopPlaceholderRipple(PreparedPlaceholderBlockId id);
private:
class Impl;
std::unique_ptr<Impl> _impl;
};
} // namespace Iv::Markdown
@@ -1108,12 +1108,18 @@ LaidOutBlock LayoutPlaceholderBlock(
const style::Markdown &markdown,
int left,
int top,
int width) {
int width,
LayoutContext context) {
auto block = LaidOutBlock();
block.kind = PreparedBlockKind::Placeholder;
block.anchorId = prepared.anchorId;
block.copyText = prepared.placeholder.copyText;
block.labelText = prepared.placeholder.label;
block.placeholderId = prepared.placeholder.id;
if (block.placeholderId && context.placeholderRuntimeFactory) {
block.placeholderRuntime = context.placeholderRuntimeFactory(
block.placeholderId);
}
block.supplementary = prepared.supplementary;
const auto &style = markdown.placeholder;
@@ -1136,6 +1142,12 @@ LaidOutBlock LayoutPlaceholderBlock(
labelHeight + style.padding.top() + style.padding.bottom());
block.mediaRect = QRect(left, top, blockWidth, mediaHeight);
block.visibleMediaRect = block.mediaRect;
if (block.placeholderRuntime
&& block.placeholderRuntime->ripple
&& block.placeholderRuntime->rippleSize != block.mediaRect.size()) {
block.placeholderRuntime->ripple = nullptr;
block.placeholderRuntime->rippleSize = QSize();
}
block.labelRect = QRect(
contentLeft,
top + std::max((mediaHeight - labelHeight) / 2, 0),
@@ -1148,6 +1160,7 @@ LaidOutBlock LayoutPlaceholderBlock(
if (prepared.placeholder.embed) {
block.activation.kind = MediaActivationKind::Embed;
block.activation.embed = *prepared.placeholder.embed;
block.activation.placeholderId = block.placeholderId;
}
auto bottom = top + mediaHeight;
@@ -1160,9 +1173,10 @@ LaidOutBlock LayoutPlaceholderBlock(
markdown,
contentLeft,
bottom,
contentWidth,
style.captionSkip,
&bottom);
contentWidth,
style.captionSkip,
&bottom,
context);
block.contentRect = QRect(
left,
@@ -73,6 +73,7 @@ struct LaidOutBlock {
QString codeLanguage;
std::optional<PreparedLink> preparedLink;
ClickHandlerPtr preparedLinkHandler;
PreparedPlaceholderBlockId placeholderId;
Spellchecker::HighlightProcessId syntaxHighlightProcessId = 0;
std::vector<LaidOutBlock> children;
std::vector<LaidOutTableRow> tableRows;
@@ -118,6 +119,7 @@ struct LaidOutBlock {
int secondarySegmentIndex = -1;
int tertiarySegmentIndex = -1;
std::shared_ptr<MediaBlock> mediaBlock;
std::shared_ptr<PlaceholderBlockRuntime> placeholderRuntime;
std::shared_ptr<PhotoRuntime> photoRuntime;
MediaActivation activation;
uint64 thumbnailPhotoId = 0;
@@ -140,6 +142,8 @@ struct LayoutContext {
bool allowAsyncSyntaxHighlighting = true;
CodeBlockSyntaxHighlightTracker *syntaxHighlightTracker = nullptr;
std::function<std::shared_ptr<MediaBlock>(const PreparedBlock&)> mediaBlockFactory;
std::function<std::shared_ptr<PlaceholderBlockRuntime>(
PreparedPlaceholderBlockId)> placeholderRuntimeFactory;
};
struct TableCellLayoutData {
@@ -247,7 +251,8 @@ void RepopulateCodeBlockLeaf(
const style::Markdown &markdown,
int left,
int top,
int width);
int width,
LayoutContext context = {});
[[nodiscard]] LaidOutBlock LayoutRelatedArticleBlock(
const PreparedBlock &prepared,
const style::Markdown &markdown,
@@ -53,7 +53,6 @@ namespace {
case PreparedBlockKind::Channel:
case PreparedBlockKind::GroupedMedia:
case PreparedBlockKind::RelatedArticle:
case PreparedBlockKind::Placeholder:
return true;
case PreparedBlockKind::Paragraph:
case PreparedBlockKind::Heading:
@@ -65,6 +64,7 @@ namespace {
case PreparedBlockKind::DisplayMath:
case PreparedBlockKind::Table:
case PreparedBlockKind::Details:
case PreparedBlockKind::Placeholder:
case PreparedBlockKind::EmbedPost:
return false;
}
@@ -904,7 +904,8 @@ void PrepareNestedContext(
markdown,
left,
top,
width);
width,
context);
case PreparedBlockKind::Details:
return LayoutDetailsBlock(
prepared,
@@ -629,6 +629,7 @@ void PaintQuoteBlock(
void PaintPlaceholderBlock(
Painter &p,
const LaidOutBlock &block,
int outerWidth,
const style::Markdown &markdown,
const MarkdownArticlePaintCaches &caches,
const PaintSelectionState &selectionState,
@@ -638,28 +639,88 @@ void PaintPlaceholderBlock(
p.save();
p.setClipRect(visible);
auto hq = PainterHighQualityEnabler(p);
const auto max = block.labelLeaf.maxWidth();
const auto radius = markdown.placeholder.padding.left();
p.setBrush(st::windowBgOver);
p.setPen(Qt::NoPen);
const auto skip = (max < block.labelRect.width())
? ((block.labelRect.width() - max) / 2)
: 0;
p.drawRoundedRect(
block.labelRect.marginsRemoved(
{ skip, 0, skip, 0 }
).marginsAdded(markdown.placeholder.padding),
radius,
radius);
p.setPen(st::windowSubTextFg->c);
PaintTextLeaf(
p,
block.labelLeaf,
caches,
block.labelRect,
block.labelWidth,
visible,
style::al_center);
if (block.activation.kind == MediaActivationKind::Embed
&& block.placeholderRuntime) {
const auto border = markdown.placeholder.border;
const auto radius = markdown.placeholder.radius;
const auto borderSkip = border / 2;
const auto borderRect = block.mediaRect.marginsRemoved(QMargins(
borderSkip,
borderSkip,
borderSkip,
borderSkip));
const auto active = ClickHandler::showAsActive(
block.placeholderRuntime->clickHandler);
const auto pressed = ClickHandler::showAsPressed(
block.placeholderRuntime->clickHandler);
if (active || pressed) {
p.setPen(Qt::NoPen);
p.setBrush(st::windowBgOver);
p.drawRoundedRect(block.mediaRect, radius, radius);
}
if (const auto &ripple = block.placeholderRuntime->ripple) {
ripple->paint(
p,
block.mediaRect.x(),
block.mediaRect.y(),
outerWidth,
&st::windowBgRipple->c);
}
auto pen = QPen(st::windowActiveTextFg->c);
pen.setWidth(border);
p.setPen(pen);
p.setBrush(Qt::NoBrush);
p.drawRoundedRect(borderRect, radius, radius);
if (block.placeholderRuntime->loading) {
const auto size = QSize(
markdown.placeholder.spinnerSize,
markdown.placeholder.spinnerSize);
const auto spinner = style::centerrect(
block.mediaRect,
QRect(QPoint(), size));
Ui::InfiniteRadialAnimation::Draw(
p,
block.placeholderRuntime->loadingAnimation.computeState(),
spinner.topLeft(),
spinner.size(),
outerWidth,
QPen(st::windowActiveTextFg->c),
markdown.placeholder.spinnerWidth);
} else {
p.setPen(st::windowActiveTextFg->c);
PaintTextLeaf(
p,
block.labelLeaf,
caches,
block.labelRect,
block.labelWidth,
visible,
style::al_center);
}
} else {
const auto max = block.labelLeaf.maxWidth();
const auto radius = markdown.placeholder.padding.left();
p.setBrush(st::windowBgOver);
p.setPen(Qt::NoPen);
const auto skip = (max < block.labelRect.width())
? ((block.labelRect.width() - max) / 2)
: 0;
p.drawRoundedRect(
block.labelRect.marginsRemoved(
{ skip, 0, skip, 0 }
).marginsAdded(markdown.placeholder.padding),
radius,
radius);
p.setPen(st::windowSubTextFg->c);
PaintTextLeaf(
p,
block.labelLeaf,
caches,
block.labelRect,
block.labelWidth,
visible,
style::al_center);
}
if (block.segmentIndex >= 0
&& WholeSegmentSelected(selectionState, block.segmentIndex)) {
p.fillRect(block.visibleMediaRect, p.textPalette().selectOverlay);
@@ -1340,6 +1401,7 @@ void PaintBlock(
PaintPlaceholderBlock(
p,
block,
outerWidth,
markdown,
caches,
selectionState,
@@ -88,6 +88,22 @@ public:
virtual void join(Qt::MouseButton button) const = 0;
};
struct PreparedMediaBlockId {
uint64 value = 0;
[[nodiscard]] explicit operator bool() const {
return (value != 0);
}
};
struct PreparedPlaceholderBlockId {
uint64 value = 0;
[[nodiscard]] explicit operator bool() const {
return (value != 0);
}
};
struct PreparedPhotoBlockData;
struct PreparedVideoBlockData;
struct PreparedAudioBlockData;
@@ -197,6 +213,7 @@ struct MediaActivation {
MediaActivationKind kind = MediaActivationKind::None;
QString url;
EmbedRequest embed;
PreparedPlaceholderBlockId placeholderId;
std::shared_ptr<PhotoRuntime> photo;
std::shared_ptr<DocumentRuntime> document;
std::shared_ptr<ChannelRuntime> channel;
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "iv/markdown/iv_markdown_embed_overlay.h"
#include "base/algorithm.h"
#include "core/file_utilities.h"
#include "lang/lang_keys.h"
#include "ui/cached_round_corners.h"
@@ -40,6 +41,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Iv::Markdown {
namespace {
constexpr auto kReadyRevealDelay = crl::time(1000);
[[nodiscard]] TextWithEntities GenericWebviewErrorText() {
return { u"Error: Could not initialize WebView."_q };
}
@@ -211,6 +214,9 @@ EmbedOverlay::EmbedOverlay(
, _linkActivationCallback(std::move(linkActivationCallback))
, _storageId(std::move(storageId))
, _dataRequestHandler(std::move(dataRequestHandler))
, _readyDelayTimer([=] {
revealReadyEmbed();
})
, _loadingAnimation(
[=] {
if (!anim::Disabled()) {
@@ -244,17 +250,41 @@ EmbedOverlay::EmbedOverlay(
EmbedOverlay::~EmbedOverlay() {
destroyWebview();
cancelReadyDelay();
removeEscapeFilter();
}
bool EmbedOverlay::preloadEmbed(
const EmbedRequest &request,
std::function<void()> shownCallback,
std::function<void()> failedCallback) {
return startEmbed(
request,
false,
std::move(shownCallback),
std::move(failedCallback));
}
bool EmbedOverlay::showEmbed(const EmbedRequest &request) {
return startEmbed(request, true, {}, {});
}
void EmbedOverlay::cancelPreload() {
closeEmbed();
}
bool EmbedOverlay::startEmbed(
const EmbedRequest &request,
bool showErrorOnFailure,
std::function<void()> shownCallback,
std::function<void()> failedCallback) {
if (!request) {
return false;
}
closeEmbed();
if (isHidden()) {
_focusRestore = QApplication::focusWidget();
}
destroyWebview();
_request = request;
_preferredBodySize = QSize();
_pendingPreferredBodySize = QSize();
@@ -263,10 +293,12 @@ bool EmbedOverlay::showEmbed(const EmbedRequest &request) {
_readyFromResource = false;
_ready = false;
_loading = true;
_showErrorOnFailure = showErrorOnFailure;
_shownCallback = std::move(shownCallback);
_failedCallback = std::move(failedCallback);
clearWebviewError();
QWidget::show();
installEscapeFilter();
raiseSurfaces();
QWidget::hide();
removeEscapeFilter();
updateContentGeometry();
_loadingAnimation.start();
_content->update();
@@ -306,9 +338,7 @@ bool EmbedOverlay::showEmbed(const EmbedRequest &request) {
}
void EmbedOverlay::closeEmbed() {
if (isHidden()) {
return;
}
cancelReadyDelay();
_loading = false;
_loadingAnimation.stop(anim::type::instant);
if (_content) {
@@ -325,6 +355,10 @@ void EmbedOverlay::closeEmbed() {
_cssToQtScale = 1.;
_readyFromResource = false;
_ready = false;
_showErrorOnFailure = false;
_shownCallback = nullptr;
_failedCallback = nullptr;
_pressedOutside = false;
restoreFocus();
}
@@ -342,11 +376,11 @@ void EmbedOverlay::testHandleWebviewMessage(const QJsonDocument &message) {
}
void EmbedOverlay::testHandleNavigationDone(bool success) {
if (success) {
if (success && _readyFromResource) {
handleWebviewMessage(NavigationReadyMessage(
_readyFromResource ? _request.resourceId : QByteArray(),
_readyFromResource ? _readyNavigationToken : QString(),
_readyFromResource ? QString() : _request.fallbackUrl));
_request.resourceId,
_readyNavigationToken,
QString()));
return;
}
handleNavigationDone(success);
@@ -356,6 +390,15 @@ bool EmbedOverlay::testLoadingCoverVisible() const {
return _loading;
}
bool EmbedOverlay::testReadyDelayScheduled() const {
return _readyDelayTimer.isActive();
}
void EmbedOverlay::testFireReadyDelay() {
cancelReadyDelay();
revealReadyEmbed();
}
const Webview::StorageId &EmbedOverlay::testEffectiveStorageId() const {
return _storageId;
}
@@ -535,16 +578,15 @@ void EmbedOverlay::handleWebviewMessage(const QJsonDocument &message) {
showWebviewError();
return;
}
if (_readyFromResource) {
if (object.value("token").toString() != _readyNavigationToken) {
return;
}
if (normalizedRequestId(
object.value("resourceId").toString().toStdString())
!= _request.resourceId) {
return;
}
} else if (object.value("url").toString().isEmpty()) {
if (!_readyFromResource) {
return;
}
if (object.value("token").toString() != _readyNavigationToken) {
return;
}
if (normalizedRequestId(
object.value("resourceId").toString().toStdString())
!= _request.resourceId) {
return;
}
setReady();
@@ -578,12 +620,22 @@ void EmbedOverlay::handleWebviewMessage(const QJsonDocument &message) {
}
void EmbedOverlay::handleNavigationDone(bool success) {
if (success || !_request || _ready || isHidden()) {
if (!_request || _ready) {
return;
}
if (success) {
if (!_readyFromResource) {
setReady();
}
return;
}
showWebviewError();
}
void EmbedOverlay::cancelReadyDelay() {
_readyDelayTimer.cancel();
}
void EmbedOverlay::setReady() {
if (_ready || !_webview || !_webview->widget()) {
return;
@@ -594,19 +646,34 @@ void EmbedOverlay::setReady() {
|| _pendingPreferredBodySize.height() > 0)) {
_preferredBodySize = _pendingPreferredBodySize;
_pendingPreferredBodySize = QSize();
updateContentGeometry();
} else {
updateWebviewGeometry();
}
updateContentGeometry();
cancelReadyDelay();
_readyDelayTimer.callOnce(kReadyRevealDelay);
}
void EmbedOverlay::revealReadyEmbed() {
if (!_ready || !_request || !_webview || !_webview->widget()) {
return;
}
cancelReadyDelay();
clearWebviewError();
QWidget::show();
installEscapeFilter();
raiseSurfaces();
updateContentGeometry();
_loading = false;
_loadingAnimation.stop(anim::type::normal);
_content->update();
clearWebviewError();
_webview->widget()->show();
_webview->widget()->raise();
_webview->focus();
update();
_content->update();
if (const auto shownCallback = base::take(_shownCallback)) {
shownCallback();
}
_failedCallback = nullptr;
}
void EmbedOverlay::applyPreferredBodySize(QSize size) {
@@ -753,11 +820,34 @@ void EmbedOverlay::showWebviewError() {
}
void EmbedOverlay::showWebviewError(const TextWithEntities &text) {
cancelReadyDelay();
_ready = false;
_loading = false;
_loadingAnimation.stop(anim::type::normal);
_content->update();
hideWebview();
destroyWebview();
_shownCallback = nullptr;
const auto failedCallback = base::take(_failedCallback);
const auto showError = !isHidden() || _showErrorOnFailure;
_showErrorOnFailure = false;
_readyNavigationToken = QString();
_readyFromResource = false;
if (!showError) {
clearWebviewError();
_request = EmbedRequest();
_preferredBodySize = QSize();
_pendingPreferredBodySize = QSize();
_cssToQtScale = 1.;
restoreFocus();
if (failedCallback) {
failedCallback();
}
return;
}
QWidget::show();
installEscapeFilter();
raiseSurfaces();
if (!_error) {
_error = Ui::CreateChild<Ui::PaddingWrap<Ui::FlatLabel>>(
_content,
@@ -783,6 +873,9 @@ void EmbedOverlay::showWebviewError(const TextWithEntities &text) {
_errorLabel->setMarkedText(AddFallbackAction(text, _request.fallbackUrl));
_error->show();
updateContentGeometry();
if (failedCallback) {
failedCallback();
}
}
void EmbedOverlay::clearWebviewError() {
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "base/flat_map.h"
#include "base/timer.h"
#include "iv/markdown/iv_markdown_common.h"
#include "ui/effects/radial_animation.h"
#include "ui/rp_widget.h"
@@ -53,12 +54,19 @@ public:
dataRequestHandler);
~EmbedOverlay();
[[nodiscard]] bool preloadEmbed(
const EmbedRequest &request,
std::function<void()> shownCallback = {},
std::function<void()> failedCallback = {});
[[nodiscard]] bool showEmbed(const EmbedRequest &request);
void cancelPreload();
void closeEmbed();
void updateGeometry(QRect geometry, int contentWidth);
void testHandleWebviewMessage(const QJsonDocument &message);
void testHandleNavigationDone(bool success);
[[nodiscard]] bool testLoadingCoverVisible() const;
[[nodiscard]] bool testReadyDelayScheduled() const;
void testFireReadyDelay();
[[nodiscard]] const Webview::StorageId &testEffectiveStorageId() const;
protected:
@@ -71,11 +79,18 @@ private:
void installEscapeFilter();
void removeEscapeFilter();
[[nodiscard]] bool eventFromOverlayWindow(QObject *object) const;
[[nodiscard]] bool startEmbed(
const EmbedRequest &request,
bool showErrorOnFailure,
std::function<void()> shownCallback,
std::function<void()> failedCallback);
void ensureWebview();
[[nodiscard]] Webview::WindowConfig makeWindowConfig() const;
void handleWebviewMessage(const QJsonDocument &message);
void handleNavigationDone(bool success);
void cancelReadyDelay();
void setReady();
void revealReadyEmbed();
void applyPreferredBodySize(QSize size);
void applyPreferredBodyHeight(int height);
void updateCssToQtScale(int viewportWidth);
@@ -106,6 +121,7 @@ private:
Ui::PaddingWrap<Ui::FlatLabel> *_error = nullptr;
Ui::FlatLabel *_errorLabel = nullptr;
std::unique_ptr<Webview::Window> _webview;
base::Timer _readyDelayTimer;
Ui::InfiniteRadialAnimation _loadingAnimation;
EmbedRequest _request;
QRect _contentGeometry;
@@ -113,6 +129,8 @@ private:
QSize _pendingPreferredBodySize;
QString _readyNavigationToken;
QPointer<QWidget> _focusRestore;
std::function<void()> _shownCallback;
std::function<void()> _failedCallback;
int _contentWidth = 0;
int _navigationGeneration = 0;
int _webviewGeneration = 0;
@@ -121,6 +139,7 @@ private:
bool _readyFromResource = false;
bool _ready = false;
bool _loading = false;
bool _showErrorOnFailure = false;
bool _escapeFilterInstalled = false;
};
@@ -116,14 +116,6 @@ struct PreparedTableRow {
bool header = false;
};
struct PreparedMediaBlockId {
uint64 value = 0;
[[nodiscard]] explicit operator bool() const {
return (value != 0);
}
};
struct PreparedPhotoBlockData {
PreparedMediaBlockId id;
uint64 photoId = 0;
@@ -194,6 +186,7 @@ struct PreparedGroupedMediaBlockData {
};
struct PreparedPlaceholderBlockData {
PreparedPlaceholderBlockId id;
QString label;
QString copyText;
std::optional<EmbedRequest> embed;
@@ -765,10 +765,11 @@ using NativeIvHtmlAttributes = std::vector<NativeIvHtmlAttribute>;
const MTPDpageBlockEmbed &data,
std::vector<PreparedBlock> *result,
NativeIvPrepareState *state) {
const auto label = tr::lng_iv_click_to_view(tr::now);
const auto html = RenderNativeIvEmbedHtml(data, state, false);
if (html.isEmpty()) {
return PrepareNativeIvPlaceholderBlock(
u"Embed Placeholder"_q,
label,
data.vcaption(),
result,
state);
@@ -787,7 +788,7 @@ using NativeIvHtmlAttributes = std::vector<NativeIvHtmlAttribute>;
.allowScrolling = data.is_allow_scrolling(),
};
return PrepareNativeIvPlaceholderBlock(
u"Embed Placeholder"_q,
label,
data.vcaption(),
result,
state,
@@ -1506,6 +1507,9 @@ void MarkNativeIvTableSlots(
data.vtext(),
state);
}, [&](const MTPDpageBlockAuthorDate &data) {
return PrepareNativeIvPlainPlaceholderBlock(
u"Unsupported Content"_q,
result); AssertIsDebug();
auto prepared = PreparedIvRichText();
auto anchorId = QString();
if (!PrepareNativeIvRichText(
@@ -20,9 +20,19 @@ struct GeoPointLocation;
namespace Iv::Markdown {
namespace {
[[nodiscard]] uint64 GeneratePreparedBlockIdValue(
NativeIvPrepareState *state) {
return uint64(++state->nextGeneratedId);
}
[[nodiscard]] PreparedMediaBlockId GeneratePreparedMediaBlockId(
NativeIvPrepareState *state) {
return { .value = uint64(++state->nextGeneratedId) };
return { .value = GeneratePreparedBlockIdValue(state) };
}
[[nodiscard]] PreparedPlaceholderBlockId GeneratePreparedPlaceholderBlockId(
NativeIvPrepareState *state) {
return { .value = GeneratePreparedBlockIdValue(state) };
}
void ShiftEntities(EntitiesInText *entities, int delta) {
@@ -850,8 +860,11 @@ bool PrepareNativeIvPlaceholderBlock(
block.links = std::move(prepared.links);
block.anchorId = std::move(anchorId);
block.supplementary = true;
block.placeholder.label = label;
block.placeholder.label = std::move(label);
block.placeholder.embed = std::move(embed);
if (block.placeholder.embed && *block.placeholder.embed) {
block.placeholder.id = GeneratePreparedPlaceholderBlockId(state);
}
block.placeholder.copyText = NativeIvPlaceholderCopyText(
block.placeholder.label,
block.text);
@@ -173,9 +173,15 @@ public:
[[nodiscard]] rpl::producer<int> scrollTopValue() const;
private:
struct PendingEmbedState {
PreparedPlaceholderBlockId placeholderId;
uint64 generation = 0;
};
void setup();
void prepareArticle();
void activateLink(const PreparedLink &link, Qt::MouseButton button);
void closeEmbed();
void openEmbedLink(QString url);
void showFootnote(const PreparedLink &link, Qt::MouseButton button);
[[nodiscard]] bool showEmbed(const MediaActivation &activation);
@@ -208,6 +214,7 @@ private:
std::shared_ptr<MarkdownArticle> _article;
QString _pendingFragment;
int _devicePixelRatio = 0;
PendingEmbedState _pendingEmbed;
};
@@ -430,13 +437,27 @@ void MarkdownPreviewRoot::activateLink(
}
}
void MarkdownPreviewRoot::closeEmbed() {
if (_body) {
_body->clearAllPlaceholderLoading();
}
const auto hadPending = bool(_pendingEmbed.placeholderId);
_pendingEmbed.placeholderId = {};
if (!_embedOverlay) {
return;
}
if (hadPending) {
_embedOverlay->cancelPreload();
} else {
_embedOverlay->closeEmbed();
}
}
void MarkdownPreviewRoot::openEmbedLink(QString url) {
if (url.isEmpty()) {
return;
}
if (_embedOverlay) {
_embedOverlay->closeEmbed();
}
closeEmbed();
HiddenUrlClickHandler::Open(url, CurrentClickHandlerContext(_options));
}
@@ -464,9 +485,45 @@ void MarkdownPreviewRoot::showFootnote(
}
bool MarkdownPreviewRoot::showEmbed(const MediaActivation &activation) {
return _embedOverlay
? _embedOverlay->showEmbed(activation.embed)
: false;
if (activation.kind != MediaActivationKind::Embed
|| !activation.embed
|| !activation.placeholderId) {
return false;
}
const auto placeholderId = activation.placeholderId;
const auto generation = ++_pendingEmbed.generation;
if (_body && _pendingEmbed.placeholderId) {
_body->clearPlaceholderLoading(_pendingEmbed.placeholderId);
}
if (_pendingEmbed.placeholderId && _embedOverlay) {
_embedOverlay->cancelPreload();
}
_pendingEmbed.placeholderId = placeholderId;
if (_body) {
_body->setPlaceholderLoading(placeholderId);
}
const auto finishPending = [=] {
if (_pendingEmbed.generation != generation
|| (_pendingEmbed.placeholderId.value != placeholderId.value)) {
return;
}
if (_body) {
_body->clearPlaceholderLoading(placeholderId);
}
_pendingEmbed.placeholderId = {};
};
if (!_embedOverlay) {
finishPending();
return false;
}
const auto started = _embedOverlay->preloadEmbed(
activation.embed,
finishPending,
finishPending);
if (!started) {
finishPending();
}
return started;
}
void MarkdownPreviewRoot::fillFootnoteBox(
@@ -509,9 +566,7 @@ void MarkdownPreviewRoot::applyPreparedContent(
int prepareMs) {
const auto failure = prepared.failure;
const auto debug = prepared.debug;
if (_embedOverlay) {
_embedOverlay->closeEmbed();
}
closeEmbed();
if (failure.failed()) {
_article = nullptr;
_footnotes.clear();
@@ -322,6 +322,41 @@ void MarkdownDocumentWidget::requestRelayout(QRect articleRect) {
});
}
void MarkdownDocumentWidget::setPlaceholderLoading(
PreparedPlaceholderBlockId id) {
if (_article) {
_article->setPlaceholderLoading(id);
}
}
void MarkdownDocumentWidget::clearPlaceholderLoading(
PreparedPlaceholderBlockId id) {
if (_article) {
_article->clearPlaceholderLoading(id);
}
}
void MarkdownDocumentWidget::clearAllPlaceholderLoading() {
if (_article) {
_article->clearAllPlaceholderLoading();
}
}
void MarkdownDocumentWidget::addPlaceholderRipple(
PreparedPlaceholderBlockId id,
QPoint point) {
if (_article) {
_article->addPlaceholderRipple(id, point);
}
}
void MarkdownDocumentWidget::stopPlaceholderRipple(
PreparedPlaceholderBlockId id) {
if (_article) {
_article->stopPlaceholderRipple(id);
}
}
void MarkdownDocumentWidget::paintEvent(QPaintEvent *e) {
if (!_article) {
return;
@@ -519,6 +554,7 @@ void MarkdownDocumentWidget::mouseDoubleClickEvent(QMouseEvent *e) {
}
void MarkdownDocumentWidget::focusOutEvent(QFocusEvent *e) {
stopPressedPlaceholderRipple();
if (!_selection.empty()) {
_savedSelection = _selection;
_savedSelectionEndpoints = _selectionEndpoints;
@@ -868,9 +904,17 @@ MarkdownArticlePaintCaches MarkdownDocumentWidget::textPaintCaches() {
};
}
void MarkdownDocumentWidget::stopPressedPlaceholderRipple() {
if (_pressedPlaceholderId) {
stopPlaceholderRipple(_pressedPlaceholderId);
_pressedPlaceholderId = {};
}
}
void MarkdownDocumentWidget::dragActionStart(
QPoint point,
Qt::MouseButton button) {
stopPressedPlaceholderRipple();
const auto state = hitTest(
point,
Ui::Text::StateRequest::Flag::LookupLink
@@ -879,6 +923,13 @@ void MarkdownDocumentWidget::dragActionStart(
if (button != Qt::LeftButton) {
return;
}
if (state.mediaActivation.kind == MediaActivationKind::Embed
&& state.mediaActivation.placeholderId) {
_pressedPlaceholderId = state.mediaActivation.placeholderId;
addPlaceholderRipple(
state.mediaActivation.placeholderId,
state.placeholderLocalPoint);
}
_dragStartPosition = point;
_dragStartHadSelection = !selectionForCopy().empty();
_selectionClickPreparedLink = (state.preparedLink
@@ -933,6 +984,7 @@ MarkdownArticleHitTestResult MarkdownDocumentWidget::dragActionFinish(
QPoint point,
Qt::MouseButton button) {
const auto state = dragActionUpdate(point);
stopPressedPlaceholderRipple();
auto activated = ClickHandler::unpressed();
const auto dragStartHadSelection = _dragStartHadSelection;
const auto toggleFromDetailsClick = !dragStartHadSelection
@@ -57,6 +57,11 @@ public:
int resizeGetHeight(int newWidth) override;
void requestRepaint(QRect articleRect) override;
void requestRelayout(QRect articleRect) override;
void setPlaceholderLoading(PreparedPlaceholderBlockId id);
void clearPlaceholderLoading(PreparedPlaceholderBlockId id);
void clearAllPlaceholderLoading();
void addPlaceholderRipple(PreparedPlaceholderBlockId id, QPoint point);
void stopPlaceholderRipple(PreparedPlaceholderBlockId id);
protected:
void paintEvent(QPaintEvent *e) override;
@@ -112,6 +117,7 @@ private:
[[nodiscard]] Ui::Text::QuotePaintCache *ensurePrePaintCache();
[[nodiscard]] Ui::Text::QuotePaintCache *ensureBlockquotePaintCache();
[[nodiscard]] MarkdownArticlePaintCaches textPaintCaches();
void stopPressedPlaceholderRipple();
void dragActionStart(QPoint point, Qt::MouseButton button);
MarkdownArticleHitTestResult dragActionUpdate(QPoint point);
MarkdownArticleHitTestResult dragActionFinish(
@@ -141,6 +147,7 @@ private:
int _dragSymbol = 0;
TextSelection _dragExpandedSelection;
std::optional<PreparedLink> _selectionClickPreparedLink;
PreparedPlaceholderBlockId _pressedPlaceholderId;
bool _dragStartHadSelection = false;
int _lastRelayoutMs = 0;
int _zoom = 100;
+416 -136
View File
@@ -1243,7 +1243,7 @@ constexpr auto kNativeIvEmbedPostAuthorPhotoId = uint64(9301);
case NativeIvPlaceholderKind::Video:
return u"Video Placeholder"_q;
case NativeIvPlaceholderKind::Embed:
return u"Embed Placeholder"_q;
return u"Click to View"_q;
case NativeIvPlaceholderKind::Collage:
return u"Collage placeholder"_q;
case NativeIvPlaceholderKind::Slideshow:
@@ -4208,6 +4208,8 @@ void CheckNativeInstantViewPrepareCoverage(bool *ok) {
const PreparedBlock *embedPost = nullptr;
const PreparedBlock *coveredPhoto = nullptr;
auto preparedPlaceholders = std::vector<const PreparedBlock*>();
auto preparedPlaceholderIds = std::vector<
std::pair<QString, PreparedPlaceholderBlockId>>();
ForEachPreparedBlock(
supported.content.blocks.blocks,
[&](const PreparedBlock &block) {
@@ -5207,12 +5209,19 @@ void CheckNativeInstantViewPrepareCoverage(bool *ok) {
== (fixture.expectedLabel + u"\n"_q + fixture.caption),
fixture.expectedLabel + u" placeholder copy text"_q,
ok);
Check(
bool(block->placeholder.id),
fixture.expectedLabel + u" placeholder id"_q,
ok);
Check(
block->placeholder.embed.has_value(),
fixture.expectedLabel + u" placeholder embed metadata"_q,
ok);
if (block->placeholder.embed) {
const auto &embed = *block->placeholder.embed;
preparedPlaceholderIds.emplace_back(
embed.fallbackUrl,
block->placeholder.id);
Check(
!embed.resourceId.isEmpty(),
fixture.expectedLabel + u" placeholder embed resource id"_q,
@@ -5470,6 +5479,12 @@ void CheckNativeInstantViewPrepareCoverage(bool *ok) {
const auto hit = placeholderArticle->hitTest(
bounds->center(),
lookupFlags);
const auto preparedId = std::find_if(
preparedPlaceholderIds.begin(),
preparedPlaceholderIds.end(),
[&](const auto &entry) {
return entry.first == fixture.expectedFallbackUrl;
});
Check(
hit.mediaActivation.kind == MediaActivationKind::Embed,
fixture.expectedLabel + u" article embed activation kind"_q,
@@ -5484,22 +5499,40 @@ void CheckNativeInstantViewPrepareCoverage(bool *ok) {
== fixture.expectedAllowScrolling,
fixture.expectedLabel + u" article embed activation scrolling"_q,
ok);
Check(
bool(hit.mediaActivation.placeholderId),
fixture.expectedLabel + u" article embed placeholder id"_q,
ok);
Check(
(preparedId != preparedPlaceholderIds.end())
&& (hit.mediaActivation.placeholderId.value
== preparedId->second.value),
fixture.expectedLabel + u" article embed placeholder id match"_q,
ok);
}
}
}
const auto makePreviewEmbedBlock = [](
QString url,
QString caption) {
return MTP_pageBlockEmbed(
MTP_flags(
MTPDpageBlockEmbed::Flag::f_allow_scrolling
| MTPDpageBlockEmbed::Flag::f_url
| MTPDpageBlockEmbed::Flag::f_w),
MTP_string(url),
MTP_string(),
MTP_long(0),
MTP_int(640),
MTP_int(0),
NativeIvCaption(caption));
};
const auto previewEmbedLabel = u"native-iv-embed-preview-overlay"_q;
const auto previewEmbedBlock = MTP_pageBlockEmbed(
MTP_flags(
MTPDpageBlockEmbed::Flag::f_allow_scrolling
| MTPDpageBlockEmbed::Flag::f_url
| MTPDpageBlockEmbed::Flag::f_w),
MTP_string("https://example.com/embed"),
MTP_string(),
MTP_long(0),
MTP_int(640),
MTP_int(0),
NativeIvCaption(u"Embed caption"_q));
const auto previewEmbedBlock = makePreviewEmbedBlock(
u"https://example.com/embed"_q,
u"Embed caption"_q);
auto previewEmbedSource = NativeIvSource(QVector<MTPPageBlock>{
previewEmbedBlock,
});
@@ -5605,20 +5638,28 @@ void CheckNativeInstantViewPrepareCoverage(bool *ok) {
previewEmbedLabel + u" click bounds"_q,
ok);
if (clickBounds) {
const auto bodyBeforeClick = RenderWidgetForTest(body);
Check(
!overlayShell->isVisible(),
previewEmbedLabel + u" overlay hidden before click"_q,
ok);
SendMouseClick(body, clickBounds->center(), Qt::LeftButton);
FlushPendingWidgetEvents();
const auto loadingBody = RenderWidgetForTest(body);
Check(
overlayShell->isVisible(),
previewEmbedLabel + u" overlay visible after click"_q,
!overlayShell->isVisible(),
previewEmbedLabel + u" overlay hidden after click"_q,
ok);
Check(
overlay->testLoadingCoverVisible(),
previewEmbedLabel
+ u" loading cover visible before navigation done"_q,
previewEmbedLabel + u" preload active after click"_q,
ok);
Check(
PixelsDifferInRect(
bodyBeforeClick,
loadingBody,
*clickBounds),
previewEmbedLabel + u" placeholder loading visible"_q,
ok);
const auto availableRect = NativeIvOverlayAvailableRect(
overlay);
@@ -5626,148 +5667,387 @@ void CheckNativeInstantViewPrepareCoverage(bool *ok) {
availableRect.isValid(),
previewEmbedLabel + u" overlay available rect"_q,
ok);
const auto overlayBeforeResize = RenderWidgetForTest(overlay);
Check(
!overlayBeforeResize.isNull(),
previewEmbedLabel
+ u" overlay render before preferred size"_q,
ok);
const auto loadingGeometry = overlayShell->geometry();
auto scrimPoint = QPoint();
auto scrimPixel = uint(0);
auto canCompareScrimPixel = false;
if (availableRect.isValid() && !overlayBeforeResize.isNull()) {
scrimPoint = QPoint(
std::max(availableRect.left() / 2, 0),
std::clamp(
availableRect.center().y(),
0,
overlayBeforeResize.height() - 1));
canCompareScrimPixel
= !overlayShell->geometry().contains(scrimPoint);
Check(
canCompareScrimPixel,
previewEmbedLabel
+ u" scrim sample sits outside shell"_q,
ok);
if (canCompareScrimPixel) {
scrimPixel = overlayBeforeResize.pixel(scrimPoint);
}
}
const auto preferredBodySize = QSize(180, 96);
overlay->testHandleWebviewMessage(
NativeIvPreferredSizeMessage(preferredBodySize));
NativeIvPreferredSizeMessage(QSize(180, 96)));
FlushPendingWidgetEvents();
Check(
!overlayShell->isVisible(),
previewEmbedLabel + u" overlay stays hidden after preferred size"_q,
ok);
Check(
!overlay->testReadyDelayScheduled(),
previewEmbedLabel + u" ready delay waits for readiness"_q,
ok);
overlay->testHandleNavigationDone(true);
FlushPendingWidgetEvents();
Check(
!overlayShell->isVisible(),
previewEmbedLabel + u" overlay stays hidden after readiness"_q,
ok);
Check(
overlay->testLoadingCoverVisible(),
previewEmbedLabel + u" preload stays active after readiness"_q,
ok);
Check(
overlay->testReadyDelayScheduled(),
previewEmbedLabel + u" ready delay scheduled after readiness"_q,
ok);
overlay->testHandleNavigationDone(false);
FlushPendingWidgetEvents();
Check(
!overlayShell->isVisible(),
previewEmbedLabel
+ u" late navigation failure keeps ready preload hidden"_q,
ok);
Check(
overlay->testLoadingCoverVisible(),
previewEmbedLabel
+ u" loading cover persists after preferred size"_q,
+ u" late navigation failure keeps preload active"_q,
ok);
Check(
overlayShell->geometry() == loadingGeometry,
overlay->testReadyDelayScheduled(),
previewEmbedLabel
+ u" overlay defers preferred size while loading"_q,
+ u" late navigation failure keeps ready delay"_q,
ok);
const auto overlayAfterResize = RenderWidgetForTest(overlay);
Check(
!overlayAfterResize.isNull(),
previewEmbedLabel
+ u" overlay render after preferred size"_q,
ok);
if (canCompareScrimPixel && !overlayAfterResize.isNull()) {
Check(
!overlayShell->geometry().contains(scrimPoint),
previewEmbedLabel
+ u" scrim sample stays outside shell"_q,
ok);
if (!overlayShell->geometry().contains(scrimPoint)) {
Check(
overlayAfterResize.pixel(scrimPoint)
== scrimPixel,
previewEmbedLabel
+ u" scrim pixel stable after resize"_q,
ok);
}
}
const auto readyGeometry = overlayShell->geometry();
if (availableRect.isValid()) {
overlay->testHandleWebviewMessage(
NativeIvPreferredSizeMessage(QSize(
availableRect.width() * 2,
availableRect.height() * 2)));
FlushPendingWidgetEvents();
}
const auto settledGeometry = overlayShell->geometry();
Check(
!overlayShell->isVisible(),
previewEmbedLabel
+ u" overlay stays hidden during settled resize"_q,
ok);
Check(
overlay->testReadyDelayScheduled(),
previewEmbedLabel + u" ready delay remains scheduled"_q,
ok);
if (availableRect.isValid()) {
Check(
overlay->testLoadingCoverVisible(),
readyGeometry != settledGeometry,
previewEmbedLabel
+ u" loading cover persists before navigation complete"_q,
+ u" hidden preload accepts later preferred size"_q,
ok);
Check(
overlayShell->geometry() == loadingGeometry,
settledGeometry == availableRect,
previewEmbedLabel
+ u" overlay defers clamped resize while loading"_q,
ok);
const auto overlayAfterClamp = RenderWidgetForTest(
overlay);
Check(
!overlayAfterClamp.isNull(),
previewEmbedLabel
+ u" overlay render after clamped resize"_q,
ok);
if (canCompareScrimPixel && !overlayAfterClamp.isNull()) {
Check(
!overlayShell->geometry().contains(scrimPoint),
previewEmbedLabel
+ u" scrim sample stays outside clamped shell"_q,
ok);
if (!overlayShell->geometry().contains(scrimPoint)) {
Check(
overlayAfterClamp.pixel(scrimPoint)
== scrimPixel,
previewEmbedLabel
+ u" scrim pixel stable after clamped resize"_q,
ok);
}
}
overlay->testHandleNavigationDone(true);
FlushPendingWidgetEvents();
Check(
!overlay->testLoadingCoverVisible(),
previewEmbedLabel
+ u" loading cover hides after navigation done"_q,
ok);
Check(
overlayShell->geometry() == availableRect,
previewEmbedLabel
+ u" overlay shell stays clamped after navigation done"_q,
ok);
overlay->closeEmbed();
FlushPendingWidgetEvents();
Check(
!overlayShell->isVisible(),
previewEmbedLabel
+ u" overlay hides before failed navigation retry"_q,
ok);
SendMouseClick(body, clickBounds->center(), Qt::LeftButton);
FlushPendingWidgetEvents();
Check(
overlay->testLoadingCoverVisible(),
previewEmbedLabel
+ u" loading cover visible before failed navigation"_q,
ok);
overlay->testHandleNavigationDone(false);
FlushPendingWidgetEvents();
const auto overlayError = preview->findChild<QWidget*>(
u"nativeIvEmbedOverlayErrorWrap"_q);
Check(
!overlay->testLoadingCoverVisible(),
previewEmbedLabel
+ u" loading cover hides after failed navigation"_q,
ok);
Check(
overlayError && overlayError->isVisible(),
previewEmbedLabel
+ u" overlay shows error after failed navigation"_q,
+ u" hidden preload clamps latest preferred size"_q,
ok);
}
overlay->testFireReadyDelay();
FlushPendingWidgetEvents();
const auto revealedBody = RenderWidgetForTest(body);
Check(
overlayShell->isVisible(),
previewEmbedLabel + u" overlay reveals after ready delay"_q,
ok);
Check(
!overlay->testLoadingCoverVisible(),
previewEmbedLabel + u" preload clears after reveal"_q,
ok);
Check(
!overlay->testReadyDelayScheduled(),
previewEmbedLabel + u" ready delay clears after reveal"_q,
ok);
Check(
overlayShell->geometry() == settledGeometry,
previewEmbedLabel
+ u" reveal uses latest settled geometry"_q,
ok);
Check(
PixelsDifferInRect(
loadingBody,
revealedBody,
*clickBounds),
previewEmbedLabel + u" placeholder loading clears on reveal"_q,
ok);
overlay->testHandleNavigationDone(false);
FlushPendingWidgetEvents();
Check(
overlayShell->isVisible(),
previewEmbedLabel
+ u" late navigation failure keeps revealed overlay"_q,
ok);
overlay->closeEmbed();
FlushPendingWidgetEvents();
Check(
!overlayShell->isVisible(),
previewEmbedLabel + u" overlay hides before failure retry"_q,
ok);
const auto bodyBeforeFailureClick = RenderWidgetForTest(body);
SendMouseClick(body, clickBounds->center(), Qt::LeftButton);
FlushPendingWidgetEvents();
const auto failureLoadingBody = RenderWidgetForTest(body);
Check(
!overlayShell->isVisible(),
previewEmbedLabel + u" overlay stays hidden on failure click"_q,
ok);
Check(
overlay->testLoadingCoverVisible(),
previewEmbedLabel + u" preload active before failure"_q,
ok);
Check(
PixelsDifferInRect(
bodyBeforeFailureClick,
failureLoadingBody,
*clickBounds),
previewEmbedLabel + u" placeholder loading visible on retry"_q,
ok);
overlay->testHandleNavigationDone(false);
FlushPendingWidgetEvents();
const auto bodyAfterFailure = RenderWidgetForTest(body);
const auto overlayError = preview->findChild<QWidget*>(
u"nativeIvEmbedOverlayErrorWrap"_q);
Check(
!overlayShell->isVisible(),
previewEmbedLabel + u" preload failure stays hidden"_q,
ok);
Check(
!overlay->testLoadingCoverVisible(),
previewEmbedLabel + u" preload clears after failure"_q,
ok);
Check(
!overlay->testReadyDelayScheduled(),
previewEmbedLabel + u" ready delay clears after failure"_q,
ok);
Check(
PixelsDifferInRect(
failureLoadingBody,
bodyAfterFailure,
*clickBounds),
previewEmbedLabel + u" placeholder loading clears on failure"_q,
ok);
Check(
!overlayError || overlayError->isHidden(),
previewEmbedLabel + u" preload failure keeps overlay error hidden"_q,
ok);
}
}
}
}
const auto previewCancelLabel = u"native-iv-embed-preview-cancel"_q;
const auto firstPreviewBlock = makePreviewEmbedBlock(
u"https://example.com/embed-first"_q,
u"First embed"_q);
const auto secondPreviewBlock = makePreviewEmbedBlock(
u"https://example.com/embed-second"_q,
u"Second embed"_q);
auto previewCancelSource = NativeIvSource(QVector<MTPPageBlock>{
firstPreviewBlock,
secondPreviewBlock,
});
const auto previewCancelPrepared = TryPrepareNativeInstantView({
.source = &previewCancelSource,
});
Check(
previewCancelPrepared.supported(),
previewCancelLabel + u" prepare supported"_q,
ok);
Check(
!previewCancelPrepared.content.failure.failed(),
previewCancelLabel + u" prepare failure"_q,
ok);
if (previewCancelPrepared.supported()
&& !previewCancelPrepared.content.failure.failed()) {
auto window = Ui::RpWindow();
window.setGeometry(QRect(0, 0, 420, 400));
window.show();
FlushPendingWidgetEvents();
auto previewOptions = OpenOptions();
previewOptions.ivWebviewStorageId = {
u"native-iv-preview-cancel"_q,
QByteArray("phase-5"),
};
auto preview = CreateMarkdownPreviewWidget(
window.body(),
std::move(previewCancelPrepared.content),
std::make_shared<MathRenderer>(),
[](Event) {
},
previewOptions);
preview->setGeometry(QRect(QPoint(), window.body()->size()));
preview->show();
FlushPendingWidgetEvents();
const auto body = FindChildObject<MarkdownDocumentWidget>(preview.get());
const auto overlay = preview->findChild<EmbedOverlay*>(
u"nativeIvEmbedOverlay"_q);
const auto overlayShell = preview->findChild<QWidget*>(
u"nativeIvEmbedOverlayShell"_q);
Check(
body != nullptr,
previewCancelLabel + u" preview body widget"_q,
ok);
Check(
overlay != nullptr,
previewCancelLabel + u" overlay object"_q,
ok);
Check(
overlayShell != nullptr,
previewCancelLabel + u" overlay shell object"_q,
ok);
if (body && overlay && overlayShell) {
auto previewProbeSource = NativeIvSource(QVector<MTPPageBlock>{
firstPreviewBlock,
secondPreviewBlock,
});
const auto previewProbePrepared = TryPrepareNativeInstantView({
.source = &previewProbeSource,
});
Check(
previewProbePrepared.supported(),
previewCancelLabel + u" probe prepare supported"_q,
ok);
Check(
!previewProbePrepared.content.failure.failed(),
previewCancelLabel + u" probe prepare failure"_q,
ok);
if (previewProbePrepared.supported()
&& !previewProbePrepared.content.failure.failed()) {
auto probeHeight = 0;
auto probeArticle = BuildArticleForTest(
std::move(previewProbePrepared.content),
std::make_shared<MathRenderer>(),
body->width(),
&probeHeight);
auto lookupFlags = Ui::Text::StateRequest::Flags();
lookupFlags |= Ui::Text::StateRequest::Flag::LookupSymbol;
const auto firstBounds = HitBoundsWhere(
probeArticle.get(),
body->width(),
probeHeight,
lookupFlags,
[](const MarkdownArticleHitTestResult &hit) {
return hit.mediaActivation.kind == MediaActivationKind::Embed
&& (hit.mediaActivation.embed.fallbackUrl
== u"https://example.com/embed-first"_q);
});
const auto secondBounds = HitBoundsWhere(
probeArticle.get(),
body->width(),
probeHeight,
lookupFlags,
[](const MarkdownArticleHitTestResult &hit) {
return hit.mediaActivation.kind == MediaActivationKind::Embed
&& (hit.mediaActivation.embed.fallbackUrl
== u"https://example.com/embed-second"_q);
});
Check(
firstBounds.has_value(),
previewCancelLabel + u" first click bounds"_q,
ok);
Check(
secondBounds.has_value(),
previewCancelLabel + u" second click bounds"_q,
ok);
if (firstBounds && secondBounds) {
const auto bodyBeforeFirstClick = RenderWidgetForTest(body);
Check(
!overlayShell->isVisible(),
previewCancelLabel + u" overlay hidden before first click"_q,
ok);
SendMouseClick(body, firstBounds->center(), Qt::LeftButton);
FlushPendingWidgetEvents();
const auto firstLoadingBody = RenderWidgetForTest(body);
Check(
!overlayShell->isVisible(),
previewCancelLabel + u" overlay hidden after first click"_q,
ok);
Check(
overlay->testLoadingCoverVisible(),
previewCancelLabel + u" first preload active"_q,
ok);
Check(
PixelsDifferInRect(
bodyBeforeFirstClick,
firstLoadingBody,
*firstBounds),
previewCancelLabel + u" first placeholder loading visible"_q,
ok);
overlay->testHandleNavigationDone(true);
FlushPendingWidgetEvents();
Check(
overlay->testReadyDelayScheduled(),
previewCancelLabel + u" first ready delay scheduled"_q,
ok);
SendMouseClick(body, secondBounds->center(), Qt::LeftButton);
FlushPendingWidgetEvents();
const auto secondLoadingBody = RenderWidgetForTest(body);
Check(
!overlayShell->isVisible(),
previewCancelLabel + u" overlay hidden after second click"_q,
ok);
Check(
overlay->testLoadingCoverVisible(),
previewCancelLabel + u" second preload active"_q,
ok);
Check(
!overlay->testReadyDelayScheduled(),
previewCancelLabel + u" first ready delay canceled"_q,
ok);
Check(
PixelsDifferInRect(
firstLoadingBody,
secondLoadingBody,
*firstBounds),
previewCancelLabel + u" first placeholder loading cleared"_q,
ok);
Check(
PixelsDifferInRect(
firstLoadingBody,
secondLoadingBody,
*secondBounds),
previewCancelLabel + u" second placeholder loading visible"_q,
ok);
overlay->testFireReadyDelay();
FlushPendingWidgetEvents();
Check(
!overlayShell->isVisible(),
previewCancelLabel + u" stale first ready delay does not reveal"_q,
ok);
Check(
overlay->testLoadingCoverVisible(),
previewCancelLabel + u" second preload stays active"_q,
ok);
overlay->testHandleWebviewMessage(
NativeIvPreferredSizeMessage(QSize(220, 120)));
FlushPendingWidgetEvents();
overlay->testHandleNavigationDone(true);
FlushPendingWidgetEvents();
Check(
overlay->testReadyDelayScheduled(),
previewCancelLabel + u" second ready delay scheduled"_q,
ok);
const auto secondSettledGeometry = overlayShell->geometry();
overlay->testFireReadyDelay();
FlushPendingWidgetEvents();
const auto revealedBody = RenderWidgetForTest(body);
Check(
overlayShell->isVisible(),
previewCancelLabel + u" second preload reveals"_q,
ok);
Check(
!overlay->testLoadingCoverVisible(),
previewCancelLabel + u" second preload clears after reveal"_q,
ok);
Check(
overlayShell->geometry() == secondSettledGeometry,
previewCancelLabel + u" second reveal uses settled geometry"_q,
ok);
Check(
PixelsDifferInRect(
secondLoadingBody,
revealedBody,
*secondBounds),
previewCancelLabel + u" second placeholder loading clears"_q,
ok);
overlay->closeEmbed();
FlushPendingWidgetEvents();
}
}
}