/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/media/history_view_photo.h" #include "history/history_item_components.h" #include "history/history_item.h" #include "history/history.h" #include "history/view/history_view_element.h" #include "history/view/history_view_cursor_state.h" #include "history/view/media/history_view_media_common.h" #include "history/view/media/history_view_media_spoiler.h" #include "media/streaming/media_streaming_instance.h" #include "media/streaming/media_streaming_player.h" #include "media/streaming/media_streaming_document.h" #include "media/streaming/media_streaming_utility.h" #include "main/main_session.h" #include "main/main_session_settings.h" #include "ui/image/image.h" #include "ui/effects/spoiler_mess.h" #include "ui/chat/chat_style.h" #include "ui/grouped_layout.h" #include "ui/cached_round_corners.h" #include "ui/painter.h" #include "ui/power_saving.h" #include "data/data_session.h" #include "data/data_stories.h" #include "data/data_streaming.h" #include "data/data_photo.h" #include "data/data_photo_media.h" #include "data/data_file_click_handler.h" #include "data/data_file_origin.h" #include "data/data_auto_download.h" #include "core/application.h" #include "styles/style_chat.h" namespace HistoryView { namespace { constexpr auto kStoryWidth = 720; constexpr auto kStoryHeight = 1280; using Data::PhotoSize; } // namespace struct Photo::Streamed { explicit Streamed(std::shared_ptr<::Media::Streaming::Document> shared); ::Media::Streaming::Instance instance; ::Media::Streaming::FrameRequest frozenRequest; QImage frozenFrame; std::array roundingCorners; QImage roundingMask; }; Photo::Streamed::Streamed( std::shared_ptr<::Media::Streaming::Document> shared) : instance(std::move(shared), nullptr) { } Photo::Photo( not_null parent, not_null realParent, not_null photo, bool spoiler) : File(parent, realParent) , _data(photo) , _storyId(realParent->media() ? realParent->media()->storyId() : FullStoryId()) , _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) , _spoiler(spoiler ? std::make_unique() : nullptr) { _caption = createCaption(realParent); create(realParent->fullId()); } Photo::Photo( not_null parent, not_null chat, not_null photo, int width) : File(parent, parent->data()) , _data(photo) , _serviceWidth(width) { create(parent->data()->fullId(), chat); } Photo::~Photo() { if (_streamed || _dataMedia) { if (_streamed) { _data->owner().streaming().keepAlive(_data); stopAnimation(); } if (_dataMedia) { _data->owner().keepAlive(base::take(_dataMedia)); _parent->checkHeavyPart(); } } togglePollingStory(false); } void Photo::create(FullMsgId contextId, PeerData *chat) { setLinks( std::make_shared( _data, crl::guard(this, [=](FullMsgId id) { showPhoto(id); }), contextId), std::make_shared(_data, contextId, chat), std::make_shared( _data, crl::guard(this, [=](FullMsgId id) { _parent->delegate()->elementCancelUpload(id); }), contextId)); if ((_dataMedia = _data->activeMediaView())) { dataMediaCreated(); } else if (_data->inlineThumbnailBytes().isEmpty() && (_data->hasExact(PhotoSize::Small) || _data->hasExact(PhotoSize::Thumbnail))) { _data->load(PhotoSize::Small, contextId); } if (_spoiler) { createSpoilerLink(_spoiler.get()); } } void Photo::ensureDataMediaCreated() const { if (_dataMedia) { return; } _dataMedia = _data->createMediaView(); dataMediaCreated(); } void Photo::dataMediaCreated() const { Expects(_dataMedia != nullptr); if (_data->inlineThumbnailBytes().isEmpty() && !_dataMedia->image(PhotoSize::Large) && !_dataMedia->image(PhotoSize::Thumbnail)) { _dataMedia->wanted(PhotoSize::Small, _realParent->fullId()); } history()->owner().registerHeavyViewPart(_parent); togglePollingStory(true); } bool Photo::hasHeavyPart() const { return (_spoiler && _spoiler->animation) || _streamed || _dataMedia; } void Photo::unloadHeavyPart() { stopAnimation(); _dataMedia = nullptr; if (_spoiler) { _spoiler->background = _spoiler->cornerCache = QImage(); _spoiler->animation = nullptr; } _imageCache = QImage(); _caption.unloadPersistentAnimation(); togglePollingStory(false); } void Photo::togglePollingStory(bool enabled) const { const auto pollingStory = (enabled ? 1 : 0); if (!_storyId || _pollingStory == pollingStory) { return; } const auto polling = Data::Stories::Polling::Chat; if (!enabled) { _data->owner().stories().unregisterPolling(_storyId, polling); } else if ( !_data->owner().stories().registerPolling(_storyId, polling)) { return; } _pollingStory = pollingStory; } QSize Photo::countOptimalSize() { if (_serviceWidth > 0) { return { int(_serviceWidth), int(_serviceWidth) }; } if (_parent->media() != this) { _caption = Ui::Text::String(); } else if (_caption.hasSkipBlock()) { _caption.updateSkipBlock( _parent->skipBlockWidth(), _parent->skipBlockHeight()); } const auto dimensions = photoSize(); const auto scaled = CountDesiredMediaSize(dimensions); const auto minWidth = std::clamp( _parent->minWidthForMedia(), (_parent->hasBubble() ? st::historyPhotoBubbleMinWidth : st::minPhotoSize), st::maxMediaSize); const auto maxActualWidth = qMax(scaled.width(), minWidth); auto maxWidth = qMax(maxActualWidth, scaled.height()); auto minHeight = qMax(scaled.height(), st::minPhotoSize); if (_parent->hasBubble() && !_caption.isEmpty()) { maxWidth = qMax( maxWidth, (st::msgPadding.left() + _caption.maxWidth() + st::msgPadding.right())); minHeight = adjustHeightForLessCrop( dimensions, { maxWidth, minHeight }); if (const auto botTop = _parent->Get()) { accumulate_max(maxWidth, botTop->maxWidth); minHeight += botTop->height; } minHeight += st::mediaCaptionSkip + _caption.minHeight(); if (isBubbleBottom()) { minHeight += st::msgPadding.bottom(); } } return { maxWidth, minHeight }; } QSize Photo::countCurrentSize(int newWidth) { if (_serviceWidth) { return { int(_serviceWidth), int(_serviceWidth) }; } const auto thumbMaxWidth = qMin(newWidth, st::maxMediaSize); const auto minWidth = std::clamp( _parent->minWidthForMedia(), (_parent->hasBubble() ? st::historyPhotoBubbleMinWidth : st::minPhotoSize), thumbMaxWidth); const auto dimensions = photoSize(); auto pix = CountPhotoMediaSize( CountDesiredMediaSize(dimensions), newWidth, maxWidth()); newWidth = qMax(pix.width(), minWidth); auto newHeight = qMax(pix.height(), st::minPhotoSize); if (_parent->hasBubble() && !_caption.isEmpty()) { auto captionMaxWidth = st::msgPadding.left() + _caption.maxWidth() + st::msgPadding.right(); const auto botTop = _parent->Get(); if (botTop) { accumulate_max(captionMaxWidth, botTop->maxWidth); } const auto maxWithCaption = qMin(st::msgMaxWidth, captionMaxWidth); newWidth = qMin(qMax(newWidth, maxWithCaption), thumbMaxWidth); newHeight = adjustHeightForLessCrop( dimensions, { newWidth, newHeight }); const auto captionw = newWidth - st::msgPadding.left() - st::msgPadding.right(); if (botTop) { newHeight += botTop->height; } newHeight += st::mediaCaptionSkip + _caption.countHeight(captionw); if (isBubbleBottom()) { newHeight += st::msgPadding.bottom(); } } return { newWidth, newHeight }; } int Photo::adjustHeightForLessCrop(QSize dimensions, QSize current) const { if (dimensions.isEmpty() || !::Media::Streaming::FrameResizeMayExpand(current, dimensions)) { return current.height(); } return qMax( current.height(), current.width() * dimensions.height() / dimensions.width()); } void Photo::draw(Painter &p, const PaintContext &context) const { if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) { return; } else if (_storyId && _data->isNull()) { return; } ensureDataMediaCreated(); _dataMedia->automaticLoad(_realParent->fullId(), _parent->data()); const auto st = context.st; const auto sti = context.imageStyle(); const auto stm = context.messageStyle(); auto loaded = _dataMedia->loaded(); auto displayLoading = _data->displayLoading(); auto inWebPage = (_parent->media() != this); auto paintx = 0, painty = 0, paintw = width(), painth = height(); auto bubble = _parent->hasBubble(); auto captionw = paintw - st::msgPadding.left() - st::msgPadding.right(); if (displayLoading) { ensureAnimation(); if (!_animation->radial.animating()) { _animation->radial.start(_dataMedia->progress()); } } const auto radial = isRadialAnimation(); const auto botTop = _parent->Get(); auto rthumb = style::rtlrect(paintx, painty, paintw, painth, width()); if (_serviceWidth > 0) { paintUserpicFrame(p, context, rthumb.topLeft()); } else { const auto rounding = inWebPage ? std::optional() : adjustedBubbleRoundingWithCaption(_caption); if (bubble) { if (!_caption.isEmpty()) { painth -= st::mediaCaptionSkip + _caption.countHeight(captionw); if (botTop) { painth -= botTop->height; } if (isBubbleBottom()) { painth -= st::msgPadding.bottom(); } rthumb = style::rtlrect(paintx, painty, paintw, painth, width()); } } else { Assert(rounding.has_value()); fillImageShadow(p, rthumb, *rounding, context); } const auto revealed = _spoiler ? _spoiler->revealAnimation.value(_spoiler->revealed ? 1. : 0.) : 1.; if (revealed < 1.) { validateSpoilerImageCache(rthumb.size(), rounding); } if (revealed > 0.) { validateImageCache(rthumb.size(), rounding); p.drawImage(rthumb.topLeft(), _imageCache); } if (revealed < 1.) { p.setOpacity(1. - revealed); p.drawImage(rthumb.topLeft(), _spoiler->background); fillImageSpoiler(p, _spoiler.get(), rthumb, context); p.setOpacity(1.); } if (context.selected()) { fillImageOverlay(p, rthumb, rounding, context); } } if (radial || (!loaded && !_data->loading())) { const auto radialOpacity = (radial && loaded && !_data->uploading()) ? _animation->radial.opacity() : 1.; const auto innerSize = st::msgFileLayout.thumbSize; QRect inner(rthumb.x() + (rthumb.width() - innerSize) / 2, rthumb.y() + (rthumb.height() - innerSize) / 2, innerSize, innerSize); p.setPen(Qt::NoPen); if (context.selected()) { p.setBrush(st->msgDateImgBgSelected()); } else if (isThumbAnimation()) { const auto over = _animation->a_thumbOver.value(1.); p.setBrush(anim::brush(st->msgDateImgBg(), st->msgDateImgBgOver(), over)); } else { const auto over = ClickHandler::showAsActive(_data->loading() ? _cancell : _savel); p.setBrush(over ? st->msgDateImgBgOver() : st->msgDateImgBg()); } p.setOpacity(radialOpacity * p.opacity()); { PainterHighQualityEnabler hq(p); p.drawEllipse(inner); } p.setOpacity(radialOpacity); const auto &icon = (radial || _data->loading()) ? sti->historyFileThumbCancel : sti->historyFileThumbDownload; icon.paintInCenter(p, inner); p.setOpacity(1); if (radial) { QRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine))); _animation->radial.draw(p, rinner, st::msgFileRadialLine, sti->historyFileThumbRadialFg); } } // date if (!_caption.isEmpty()) { p.setPen(stm->historyTextFg); _parent->prepareCustomEmojiPaint(p, context, _caption); auto top = painty + painth + st::mediaCaptionSkip; if (botTop) { botTop->text.drawLeftElided( p, st::msgPadding.left(), top, captionw, _parent->width()); top += botTop->height; } _caption.draw(p, { .position = QPoint(st::msgPadding.left(), top), .availableWidth = captionw, .palette = &stm->textPalette, .pre = stm->preCache.get(), .blockquote = stm->blockquoteCache.get(), .colors = context.st->highlightColors(), .spoiler = Ui::Text::DefaultSpoilerCache(), .now = context.now, .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), .selection = context.selection, }); } else if (!inWebPage) { auto fullRight = paintx + paintw; auto fullBottom = painty + painth; if (needInfoDisplay()) { _parent->drawInfo( p, context, fullRight, fullBottom, 2 * paintx + paintw, InfoDisplayType::Image); } if (const auto size = bubble ? std::nullopt : _parent->rightActionSize()) { auto fastShareLeft = (fullRight + st::historyFastShareLeft); auto fastShareTop = (fullBottom - st::historyFastShareBottom - size->height()); _parent->drawRightAction(p, context, fastShareLeft, fastShareTop, 2 * paintx + paintw); } } } void Photo::validateUserpicImageCache(QSize size, bool forum) const { const auto forumValue = forum ? 1 : 0; const auto large = _dataMedia->image(PhotoSize::Large); const auto ratio = style::DevicePixelRatio(); const auto blurredValue = large ? 0 : 1; if (_imageCache.size() == (size * ratio) && _imageCacheForum == forumValue && _imageCacheBlurred == blurredValue) { return; } auto original = [&] { if (large) { return large->original(); } else if (const auto thumbnail = _dataMedia->image( PhotoSize::Thumbnail)) { return thumbnail->original(); } else if (const auto small = _dataMedia->image( PhotoSize::Small)) { return small->original(); } else if (const auto blurred = _dataMedia->thumbnailInline()) { return blurred->original(); } else { return Image::Empty()->original(); } }(); auto args = Images::PrepareArgs(); if (blurredValue) { args = args.blurred(); } original = Images::Prepare(std::move(original), size * ratio, args); if (forumValue) { original = Images::Round( std::move(original), Images::CornersMask(std::min(size.width(), size.height()) * Ui::ForumUserpicRadiusMultiplier())); } else { original = Images::Circle(std::move(original)); } _imageCache = std::move(original); _imageCacheForum = forumValue; _imageCacheBlurred = blurredValue; } void Photo::validateImageCache( QSize outer, std::optional rounding) const { const auto large = _dataMedia->image(PhotoSize::Large); const auto ratio = style::DevicePixelRatio(); const auto blurredValue = large ? 0 : 1; if (_imageCache.size() == (outer * ratio) && _imageCacheRounding == rounding && _imageCacheBlurred == blurredValue) { return; } _imageCache = Images::Round( prepareImageCache(outer), MediaRoundingMask(rounding)); _imageCacheRounding = rounding; _imageCacheBlurred = blurredValue; } void Photo::validateSpoilerImageCache( QSize outer, std::optional rounding) const { Expects(_spoiler != nullptr); const auto ratio = style::DevicePixelRatio(); if (_spoiler->background.size() == (outer * ratio) && _spoiler->backgroundRounding == rounding) { return; } _spoiler->background = Images::Round( prepareImageCacheWithLarge(outer, nullptr), MediaRoundingMask(rounding)); _spoiler->backgroundRounding = rounding; } QImage Photo::prepareImageCache(QSize outer) const { return prepareImageCacheWithLarge( outer, _dataMedia->image(PhotoSize::Large)); } QImage Photo::prepareImageCacheWithLarge(QSize outer, Image *large) const { using Size = PhotoSize; auto blurred = (Image*)nullptr; if (const auto embedded = _dataMedia->thumbnailInline()) { blurred = embedded; } else if (const auto thumbnail = _dataMedia->image(Size::Thumbnail)) { blurred = thumbnail; } else if (const auto small = _dataMedia->image(Size::Small)) { blurred = small; } else { blurred = large; } const auto resize = large ? ::Media::Streaming::DecideFrameResize(outer, large->size()) : ::Media::Streaming::ExpandDecision(); return PrepareWithBlurredBackground(outer, resize, large, blurred); } void Photo::paintUserpicFrame( Painter &p, QPoint photoPosition, bool markFrameShown) const { const auto autoplay = _data->videoCanBePlayed() && videoAutoplayEnabled(); const auto startPlay = autoplay && !_streamed; if (startPlay) { const_cast(this)->playAnimation(true); } else { checkStreamedIsStarted(); } const auto size = QSize(width(), height()); const auto rect = QRect(photoPosition, size); const auto forum = _parent->data()->history()->isForum(); if (_streamed && _streamed->instance.player().ready() && !_streamed->instance.player().videoSize().isEmpty()) { const auto ratio = style::DevicePixelRatio(); auto request = ::Media::Streaming::FrameRequest(); request.outer = request.resize = size * ratio; if (forum) { const auto radius = int(std::min(size.width(), size.height()) * Ui::ForumUserpicRadiusMultiplier()); if (_streamed->roundingCorners[0].width() != radius * ratio) { _streamed->roundingCorners = Images::CornersMask(radius); } request.rounding = Images::CornersMaskRef( _streamed->roundingCorners); } else { if (_streamed->roundingMask.size() != request.outer) { _streamed->roundingMask = Images::EllipseMask(size); } request.mask = _streamed->roundingMask; } if (_streamed->instance.playerLocked()) { if (_streamed->frozenFrame.isNull() || _streamed->frozenRequest != request) { _streamed->frozenRequest = request; _streamed->frozenFrame = _streamed->instance.frame(request); } p.drawImage(rect, _streamed->frozenFrame); } else { _streamed->frozenFrame = QImage(); p.drawImage(rect, _streamed->instance.frame(request)); if (markFrameShown) { _streamed->instance.markFrameShown(); } } return; } validateUserpicImageCache(size, forum); p.drawImage(rect, _imageCache); } void Photo::paintUserpicFrame( Painter &p, const PaintContext &context, QPoint photoPosition) const { paintUserpicFrame(p, photoPosition, !context.paused); if (_data->videoCanBePlayed() && !_streamed) { const auto st = context.st; const auto sti = context.imageStyle(); const auto innerSize = st::msgFileLayout.thumbSize; auto inner = QRect( photoPosition.x() + (width() - innerSize) / 2, photoPosition.y() + (height() - innerSize) / 2, innerSize, innerSize); p.setPen(Qt::NoPen); if (context.selected()) { p.setBrush(st->msgDateImgBgSelected()); } else { const auto over = ClickHandler::showAsActive(_openl); p.setBrush(over ? st->msgDateImgBgOver() : st->msgDateImgBg()); } { PainterHighQualityEnabler hq(p); p.drawEllipse(inner); } sti->historyFileThumbPlay.paintInCenter(p, inner); } } QSize Photo::photoSize() const { if (_storyId) { return { kStoryWidth, kStoryHeight }; } return QSize(_data->width(), _data->height()); } TextState Photo::textState(QPoint point, StateRequest request) const { auto result = TextState(_parent); if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) { return result; } else if (_storyId && _data->isNull()) { return result; } auto paintx = 0, painty = 0, paintw = width(), painth = height(); auto bubble = _parent->hasBubble(); if (bubble && !_caption.isEmpty()) { const auto captionw = paintw - st::msgPadding.left() - st::msgPadding.right(); painth -= _caption.countHeight(captionw); if (isBubbleBottom()) { painth -= st::msgPadding.bottom(); } if (QRect(st::msgPadding.left(), painth, captionw, height() - painth).contains(point)) { result = TextState(_parent, _caption.getState( point - QPoint(st::msgPadding.left(), painth), captionw, request.forText())); return result; } if (const auto botTop = _parent->Get()) { painth -= botTop->height; } painth -= st::mediaCaptionSkip; } if (QRect(paintx, painty, paintw, painth).contains(point)) { ensureDataMediaCreated(); result.link = (_spoiler && !_spoiler->revealed) ? _spoiler->link : _data->uploading() ? _cancell : _dataMedia->loaded() ? _openl : _data->loading() ? _cancell : _savel; } if (_caption.isEmpty() && _parent->media() == this) { auto fullRight = paintx + paintw; auto fullBottom = painty + painth; const auto bottomInfoResult = _parent->bottomInfoTextState( fullRight, fullBottom, point, InfoDisplayType::Image); if (bottomInfoResult.link || bottomInfoResult.cursor != CursorState::None || bottomInfoResult.customTooltip) { return bottomInfoResult; } if (const auto size = bubble ? std::nullopt : _parent->rightActionSize()) { auto fastShareLeft = (fullRight + st::historyFastShareLeft); auto fastShareTop = (fullBottom - st::historyFastShareBottom - size->height()); if (QRect(fastShareLeft, fastShareTop, size->width(), size->height()).contains(point)) { result.link = _parent->rightActionLink(point - QPoint(fastShareLeft, fastShareTop)); } } } return result; } QSize Photo::sizeForGroupingOptimal(int maxWidth) const { const auto size = photoSize(); return { std::max(size.width(), 1), std::max(size.height(), 1)}; } QSize Photo::sizeForGrouping(int width) const { return sizeForGroupingOptimal(width); } void Photo::drawGrouped( Painter &p, const PaintContext &context, const QRect &geometry, RectParts sides, Ui::BubbleRounding rounding, float64 highlightOpacity, not_null cacheKey, not_null cache) const { ensureDataMediaCreated(); _dataMedia->automaticLoad(_realParent->fullId(), _parent->data()); const auto st = context.st; const auto sti = context.imageStyle(); const auto loaded = _dataMedia->loaded(); const auto displayLoading = _data->displayLoading(); if (displayLoading) { ensureAnimation(); if (!_animation->radial.animating()) { _animation->radial.start(_dataMedia->progress()); } } const auto radial = isRadialAnimation(); const auto revealed = _spoiler ? _spoiler->revealAnimation.value(_spoiler->revealed ? 1. : 0.) : 1.; if (revealed < 1.) { validateSpoilerImageCache(geometry.size(), rounding); } if (revealed > 0.) { validateGroupedCache(geometry, rounding, cacheKey, cache); p.drawPixmap(geometry.topLeft(), *cache); } if (revealed < 1.) { p.setOpacity(1. - revealed); p.drawImage(geometry.topLeft(), _spoiler->background); fillImageSpoiler(p, _spoiler.get(), geometry, context); p.setOpacity(1.); } const auto overlayOpacity = context.selected() ? (1. - highlightOpacity) : highlightOpacity; if (overlayOpacity > 0.) { p.setOpacity(overlayOpacity); fillImageOverlay(p, geometry, rounding, context); if (!context.selected()) { fillImageOverlay(p, geometry, rounding, context); } p.setOpacity(1.); } const auto displayState = radial || (!loaded && !_data->loading()) || _data->waitingForAlbum(); if (displayState) { const auto radialOpacity = radial ? _animation->radial.opacity() : 1.; const auto backOpacity = (loaded && !_data->uploading()) ? radialOpacity : 1.; const auto radialSize = st::historyGroupRadialSize; const auto inner = QRect( geometry.x() + (geometry.width() - radialSize) / 2, geometry.y() + (geometry.height() - radialSize) / 2, radialSize, radialSize); p.setPen(Qt::NoPen); if (context.selected()) { p.setBrush(st->msgDateImgBgSelected()); } else if (isThumbAnimation()) { auto over = _animation->a_thumbOver.value(1.); p.setBrush(anim::brush(st->msgDateImgBg(), st->msgDateImgBgOver(), over)); } else { auto over = ClickHandler::showAsActive(_data->loading() ? _cancell : _savel); p.setBrush(over ? st->msgDateImgBgOver() : st->msgDateImgBg()); } p.setOpacity(backOpacity * p.opacity()); { PainterHighQualityEnabler hq(p); p.drawEllipse(inner); } const auto &icon = _data->waitingForAlbum() ? sti->historyFileThumbWaiting : (radial || _data->loading()) ? sti->historyFileThumbCancel : sti->historyFileThumbDownload; const auto previous = _data->waitingForAlbum() ? &sti->historyFileThumbCancel : nullptr; p.setOpacity(backOpacity); if (previous && radialOpacity > 0. && radialOpacity < 1.) { PaintInterpolatedIcon(p, icon, *previous, radialOpacity, inner); } else { icon.paintInCenter(p, inner); } p.setOpacity(1); if (radial) { const auto line = st::historyGroupRadialLine; const auto rinner = inner.marginsRemoved({ line, line, line, line }); _animation->radial.draw(p, rinner, line, sti->historyFileThumbRadialFg); } } } TextState Photo::getStateGrouped( const QRect &geometry, RectParts sides, QPoint point, StateRequest request) const { if (!geometry.contains(point)) { return {}; } ensureDataMediaCreated(); return TextState(_parent, (_spoiler && !_spoiler->revealed) ? _spoiler->link : _data->uploading() ? _cancell : _dataMedia->loaded() ? _openl : _data->loading() ? _cancell : _savel); } float64 Photo::dataProgress() const { ensureDataMediaCreated(); return _dataMedia->progress(); } bool Photo::dataFinished() const { return !_data->loading() && (!_data->uploading() || _data->waitingForAlbum()); } bool Photo::dataLoaded() const { ensureDataMediaCreated(); return _dataMedia->loaded(); } bool Photo::needInfoDisplay() const { if (_parent->data()->isFakeBotAbout()) { return false; } return _parent->data()->isSending() || _parent->data()->hasFailed() || _parent->isUnderCursor() || _parent->isLastAndSelfMessage(); } void Photo::validateGroupedCache( const QRect &geometry, Ui::BubbleRounding rounding, not_null cacheKey, not_null cache) const { using Option = Images::Option; ensureDataMediaCreated(); const auto loaded = _dataMedia->loaded(); const auto loadLevel = loaded ? 2 : (_dataMedia->thumbnailInline() || _dataMedia->image(PhotoSize::Small) || _dataMedia->image(PhotoSize::Thumbnail)) ? 1 : 0; const auto width = geometry.width(); const auto height = geometry.height(); const auto options = (loaded ? Option() : Option::Blur); const auto key = (uint64(width) << 48) | (uint64(height) << 32) | (uint64(options) << 16) | (uint64(rounding.key()) << 8) | (uint64(loadLevel)); if (*cacheKey == key) { return; } const auto unscaled = photoSize(); const auto originalWidth = style::ConvertScale(unscaled.width()); const auto originalHeight = style::ConvertScale(unscaled.height()); const auto pixSize = Ui::GetImageScaleSizeForGeometry( { originalWidth, originalHeight }, { width, height }); const auto ratio = style::DevicePixelRatio(); const auto image = _dataMedia->image(PhotoSize::Large) ? _dataMedia->image(PhotoSize::Large) : _dataMedia->image(PhotoSize::Thumbnail) ? _dataMedia->image(PhotoSize::Thumbnail) : _dataMedia->image(PhotoSize::Small) ? _dataMedia->image(PhotoSize::Small) : _dataMedia->thumbnailInline() ? _dataMedia->thumbnailInline() : Image::BlankMedia().get(); *cacheKey = key; auto scaled = Images::Prepare( image->original(), pixSize * ratio, { .options = options, .outer = { width, height } }); auto rounded = Images::Round( std::move(scaled), MediaRoundingMask(rounding)); *cache = Ui::PixmapFromImage(std::move(rounded)); } bool Photo::createStreamingObjects() { using namespace ::Media::Streaming; setStreamed(std::make_unique( history()->owner().streaming().sharedDocument( _data, _realParent->fullId()))); _streamed->instance.player().updates( ) | rpl::start_with_next_error([=](Update &&update) { handleStreamingUpdate(std::move(update)); }, [=](Error &&error) { handleStreamingError(std::move(error)); }, _streamed->instance.lifetime()); if (_streamed->instance.ready()) { streamingReady(base::duplicate(_streamed->instance.info())); } if (!_streamed->instance.valid()) { stopAnimation(); return false; } checkStreamedIsStarted(); return true; } void Photo::setStreamed(std::unique_ptr value) { const auto removed = (_streamed && !value); const auto set = (!_streamed && value); _streamed = std::move(value); if (set) { history()->owner().registerHeavyViewPart(_parent); togglePollingStory(true); } else if (removed) { _parent->checkHeavyPart(); } } void Photo::handleStreamingUpdate(::Media::Streaming::Update &&update) { using namespace ::Media::Streaming; v::match(update.data, [&](Information &update) { streamingReady(std::move(update)); }, [&](const PreloadedVideo &update) { }, [&](const UpdateVideo &update) { repaintStreamedContent(); }, [&](const PreloadedAudio &update) { }, [&](const UpdateAudio &update) { }, [&](const WaitingForData &update) { }, [&](MutedByOther) { }, [&](Finished) { }); } void Photo::handleStreamingError(::Media::Streaming::Error &&error) { _data->setVideoPlaybackFailed(); stopAnimation(); } void Photo::repaintStreamedContent() { if (_streamed && !_streamed->frozenFrame.isNull()) { return; } else if (_parent->delegate()->elementAnimationsPaused()) { return; } repaint(); } void Photo::streamingReady(::Media::Streaming::Information &&info) { repaint(); } void Photo::checkAnimation() { if (_streamed && !videoAutoplayEnabled()) { stopAnimation(); } } void Photo::stopAnimation() { setStreamed(nullptr); } void Photo::playAnimation(bool autoplay) { ensureDataMediaCreated(); if (_streamed && autoplay) { return; } else if (_streamed && videoAutoplayEnabled()) { showPhoto(_parent->data()->fullId()); return; } if (_streamed) { stopAnimation(); } else if (_data->videoCanBePlayed()) { if (!videoAutoplayEnabled()) { history()->owner().checkPlayingAnimations(); } if (!createStreamingObjects()) { _data->setVideoPlaybackFailed(); return; } } } void Photo::checkStreamedIsStarted() const { if (!_streamed) { return; } else if (_streamed->instance.paused()) { _streamed->instance.resume(); } if (_streamed && !_streamed->instance.active() && !_streamed->instance.failed()) { const auto position = _data->videoStartPosition(); auto options = ::Media::Streaming::PlaybackOptions(); options.position = position; options.mode = ::Media::Streaming::Mode::Video; options.loop = true; _streamed->instance.play(options); } } bool Photo::videoAutoplayEnabled() const { return Data::AutoDownload::ShouldAutoPlay( _data->session().settings().autoDownload(), _realParent->history()->peer, _data); } TextForMimeData Photo::selectedText(TextSelection selection) const { return _caption.toTextForMimeData(selection); } void Photo::hideSpoilers() { _caption.setSpoilerRevealed(false, anim::type::instant); if (_spoiler) { _spoiler->revealed = false; } } bool Photo::needsBubble() const { if (_storyId || !_caption.isEmpty()) { return true; } const auto item = _parent->data(); return !item->isService() && (item->repliesAreComments() || item->externalReply() || item->viaBot() || _parent->displayedReply() || _parent->displayForwardedFrom() || _parent->displayFromName() || _parent->displayedTopicButton()); } QPoint Photo::resolveCustomInfoRightBottom() const { const auto skipx = (st::msgDateImgDelta + st::msgDateImgPadding.x()); const auto skipy = (st::msgDateImgDelta + st::msgDateImgPadding.y()); return QPoint(width() - skipx, height() - skipy); } bool Photo::isReadyForOpen() const { ensureDataMediaCreated(); return _dataMedia->loaded(); } void Photo::parentTextUpdated() { _caption = (_parent->media() == this) ? createCaption(_parent->data()) : Ui::Text::String(); history()->owner().requestViewResize(_parent); } void Photo::showPhoto(FullMsgId id) { _parent->delegate()->elementOpenPhoto(_data, id); } } // namespace HistoryView