/* 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_gif.h" #include "apiwrap.h" #include "api/api_transcribes.h" #include "lang/lang_keys.h" #include "mainwindow.h" #include "main/main_session.h" #include "main/main_session_settings.h" #include "media/audio/media_audio.h" #include "media/clip/media_clip_reader.h" #include "media/player/media_player_instance.h" #include "media/streaming/media_streaming_instance.h" #include "media/streaming/media_streaming_player.h" #include "media/streaming/media_streaming_utility.h" #include "media/view/media_view_playback_progress.h" #include "ui/boxes/confirm_box.h" #include "ui/painter.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/history_view_transcribe_button.h" #include "history/view/media/history_view_media_common.h" #include "history/view/media/history_view_media_spoiler.h" #include "window/window_session_controller.h" #include "core/application.h" // Application::showDocument. #include "ui/chat/attach/attach_prepare.h" #include "ui/chat/chat_style.h" #include "ui/image/image.h" #include "ui/text/format_values.h" #include "ui/grouped_layout.h" #include "ui/cached_round_corners.h" #include "ui/power_saving.h" #include "ui/effects/path_shift_gradient.h" #include "ui/effects/spoiler_mess.h" #include "data/data_session.h" #include "data/data_stories.h" #include "data/data_streaming.h" #include "data/data_document.h" #include "data/data_file_click_handler.h" #include "data/data_file_origin.h" #include "data/data_document_media.h" #include "styles/style_chat.h" namespace HistoryView { namespace { constexpr auto kMaxGifForwardedBarLines = 4; constexpr auto kUseNonBlurredThreshold = 240; constexpr auto kMaxInlineArea = 1920 * 1080; int gifMaxStatusWidth(DocumentData *document) { auto result = st::normalFont->width(Ui::FormatDownloadText(document->size, document->size)); accumulate_max(result, st::normalFont->width(Ui::FormatGifAndSizeText(document->size))); return result; } } // namespace struct Gif::Streamed { Streamed( std::shared_ptr<::Media::Streaming::Document> shared, Fn waitingCallback); ::Media::Streaming::Instance instance; ::Media::Streaming::FrameRequest frozenRequest; QImage frozenFrame; QString frozenStatusText; }; Gif::Streamed::Streamed( std::shared_ptr<::Media::Streaming::Document> shared, Fn waitingCallback) : instance(std::move(shared), std::move(waitingCallback)) { } Gif::Gif( not_null parent, not_null realParent, not_null document, bool spoiler) : File(parent, realParent) , _data(document) , _storyId(realParent->media() ? realParent->media()->storyId() : FullStoryId()) , _caption( st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) , _spoiler(spoiler ? std::make_unique() : nullptr) , _downloadSize(Ui::FormatSizeText(_data->size)) { setDocumentLinks(_data, realParent, [=] { if (!_data->createMediaView()->canBePlayed(realParent) || !_data->isAnimation() || _data->isVideoMessage() || !CanPlayInline(_data)) { return false; } playAnimation(false); return true; }); setStatusSize(Ui::FileStatusSizeReady); if (_spoiler) { createSpoilerLink(_spoiler.get()); } refreshCaption(); if ((_dataMedia = _data->activeMediaView())) { dataMediaCreated(); } else { _data->loadThumbnail(realParent->fullId()); if (!autoplayEnabled()) { _data->loadVideoThumbnail(realParent->fullId()); } } ensureTranscribeButton(); } Gif::~Gif() { if (_streamed || _dataMedia) { if (_streamed) { _data->owner().streaming().keepAlive(_data); setStreamed(nullptr); } if (_dataMedia) { _data->owner().keepAlive(base::take(_dataMedia)); _parent->checkHeavyPart(); } } togglePollingStory(false); } bool Gif::CanPlayInline(not_null document) { const auto dimensions = document->dimensions; return dimensions.width() * dimensions.height() <= kMaxInlineArea; } QSize Gif::sizeForAspectRatio() const { // We use size only for aspect ratio and we want to have it // as close to the thumbnail as possible. //if (!_data->dimensions.isEmpty()) { // return _data->dimensions; //} if (_data->hasThumbnail()) { const auto &location = _data->thumbnailLocation(); return { location.width(), location.height() }; } return { 1, 1 }; } QSize Gif::countThumbSize(int &inOutWidthMax) const { const auto maxSize = _data->isVideoFile() ? st::maxMediaSize : _data->isVideoMessage() ? st::maxVideoMessageSize : st::maxGifSize; const auto size = style::ConvertScale(videoSize()); accumulate_min(inOutWidthMax, maxSize); return DownscaledSize(size, { inOutWidthMax, maxSize }); } QSize Gif::countOptimalSize() { if (_parent->media() != this) { _caption = Ui::Text::String(); } else if (_caption.hasSkipBlock()) { _caption.updateSkipBlock( _parent->skipBlockWidth(), _parent->skipBlockHeight()); } if (_data->isVideoMessage() && _transcribe) { const auto &entry = _data->session().api().transcribes().entry( _realParent); _transcribe->setLoading( entry.shown && (entry.requestId || entry.pending), [=] { repaint(); }); } const auto minWidth = std::clamp( _parent->minWidthForMedia(), (_parent->hasBubble() ? st::historyPhotoBubbleMinWidth : st::minPhotoSize), st::maxMediaSize); auto thumbMaxWidth = st::msgMaxWidth; const auto scaled = countThumbSize(thumbMaxWidth); auto maxWidth = std::min( std::max(scaled.width(), minWidth), thumbMaxWidth); auto minHeight = qMax(scaled.height(), st::minPhotoSize); if (!activeCurrentStreamed()) { accumulate_max(maxWidth, gifMaxStatusWidth(_data) + 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x())); } if (_parent->hasBubble()) { if (!_caption.isEmpty()) { maxWidth = qMax(maxWidth, st::msgPadding.left() + _caption.maxWidth() + st::msgPadding.right()); minHeight = adjustHeightForLessCrop( scaled, { 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(); } } } else if (isUnwrapped()) { const auto item = _parent->data(); auto via = item->Get(); auto reply = _parent->displayedReply(); auto forwarded = item->Get(); if (forwarded) { forwarded->create(via); } maxWidth += additionalWidth(via, reply, forwarded); accumulate_max(maxWidth, _parent->reactionsOptimalWidth()); } return { maxWidth, minHeight }; } QSize Gif::countCurrentSize(int newWidth) { auto availableWidth = newWidth; auto thumbMaxWidth = newWidth; const auto scaled = countThumbSize(thumbMaxWidth); const auto minWidthByInfo = _parent->infoWidth() + 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x()); newWidth = std::clamp( std::max(scaled.width(), minWidthByInfo), st::minPhotoSize, thumbMaxWidth); auto newHeight = qMax(scaled.height(), st::minPhotoSize); if (!activeCurrentStreamed()) { accumulate_max(newWidth, gifMaxStatusWidth(_data) + 2 * (st::msgDateImgDelta + st::msgDateImgPadding.x())); } if (_parent->hasBubble()) { accumulate_max(newWidth, _parent->minWidthForMedia()); if (!_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( scaled, { 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(); } } } else if (isUnwrapped()) { accumulate_max(newWidth, _parent->reactionsOptimalWidth()); const auto item = _parent->data(); auto via = item->Get(); auto reply = _parent->displayedReply(); auto forwarded = item->Get(); if (via || reply || forwarded) { auto additional = additionalWidth(via, reply, forwarded); newWidth += additional; accumulate_min(newWidth, availableWidth); auto usew = maxWidth() - additional; auto availw = newWidth - usew - st::msgReplyPadding.left() - st::msgReplyPadding.left() - st::msgReplyPadding.left(); if (!forwarded && via) { via->resize(availw); } if (reply) { [[maybe_unused]] int height = reply->resizeToWidth(availw); } } } return { newWidth, newHeight }; } int Gif::adjustHeightForLessCrop(QSize dimensions, QSize current) const { if (dimensions.isEmpty()) { return current.height(); } // Allow some more vertical space for less cropping, // but not more than 1.33 * existing height. return qMax( current.height(), qMin( current.width() * dimensions.height() / dimensions.width(), current.height() * 4 / 3)); } QSize Gif::videoSize() const { if (const auto streamed = activeCurrentStreamed()) { return streamed->player().videoSize(); } else if (!_data->dimensions.isEmpty()) { return _data->dimensions; } else if (_data->hasThumbnail()) { const auto &location = _data->thumbnailLocation(); return QSize(location.width(), location.height()); } else { return QSize(1, 1); } } void Gif::validateRoundingMask(QSize size) const { if (_roundingMask.size() != size) { const auto ratio = style::DevicePixelRatio(); _roundingMask = Images::EllipseMask(size / ratio); } } bool Gif::downloadInCorner() const { return _data->isVideoFile() && (_data->loading() || !autoplayEnabled()) && _realParent->allowsForward() && _data->canBeStreamed(_realParent) && !_data->inappPlaybackFailed(); } bool Gif::autoplayEnabled() const { return Data::AutoDownload::ShouldAutoPlay( _data->session().settings().autoDownload(), _realParent->history()->peer, _data); } void Gif::draw(Painter &p, const PaintContext &context) const { if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return; ensureDataMediaCreated(); const auto item = _parent->data(); const auto loaded = dataLoaded(); const auto displayLoading = (item->isSending() || _data->displayLoading()); const auto st = context.st; const auto sti = context.imageStyle(); const auto stm = context.messageStyle(); const auto cornerDownload = downloadInCorner(); const auto canBePlayed = _dataMedia->canBePlayed(_realParent); const auto autoplay = autoplayEnabled() && canBePlayed && CanPlayInline(_data); const auto activeRoundPlaying = activeRoundStreamed(); auto paintx = 0, painty = 0, paintw = width(), painth = height(); auto captionw = paintw - st::msgPadding.left() - st::msgPadding.right(); const bool bubble = _parent->hasBubble(); const auto outbg = context.outbg; const auto inWebPage = (_parent->media() != this); const auto isRound = _data->isVideoMessage(); const auto botTop = _parent->Get(); const auto rounding = inWebPage ? std::optional() : adjustedBubbleRoundingWithCaption(_caption); if (bubble) { if (!_caption.isEmpty()) { if (botTop) { painth -= botTop->height; } painth -= st::mediaCaptionSkip + _caption.countHeight(captionw); if (isBubbleBottom()) { painth -= st::msgPadding.bottom(); } } } auto usex = 0, usew = paintw; const auto unwrapped = isUnwrapped(); const auto via = unwrapped ? item->Get() : nullptr; const auto reply = unwrapped ? _parent->displayedReply() : nullptr; const auto forwarded = unwrapped ? item->Get() : nullptr; const auto rightAligned = unwrapped && outbg && !_parent->delegate()->elementIsChatWide(); if (via || reply || forwarded) { usew = maxWidth() - additionalWidth(via, reply, forwarded); if (rightAligned) { usex = width() - usew; } } if (isRound) { accumulate_min(usew, painth); } if (rtl()) usex = width() - usex - usew; QRect rthumb(style::rtlrect(usex + paintx, painty, usew, painth, width())); const auto revealed = (!isRound && _spoiler) ? _spoiler->revealAnimation.value(_spoiler->revealed ? 1. : 0.) : 1.; const auto fullHiddenBySpoiler = (revealed == 0.); if (revealed < 1.) { validateSpoilerImageCache(rthumb.size(), rounding); } const auto startPlay = autoplay && !_streamed && !activeRoundPlaying && !fullHiddenBySpoiler; if (startPlay) { const_cast(this)->playAnimation(true); } else { checkStreamedIsStarted(); } const auto streamingMode = _streamed || activeRoundPlaying || autoplay; const auto activeOwnPlaying = activeOwnStreamed(); auto displayMute = false; const auto streamed = activeRoundPlaying ? activeRoundPlaying : activeOwnPlaying ? &activeOwnPlaying->instance : nullptr; const auto streamedForWaiting = activeRoundPlaying ? activeRoundPlaying : _streamed ? &_streamed->instance : nullptr; if (displayLoading && (!streamedForWaiting || item->isSending() || _data->uploading() || (cornerDownload && _data->loading()))) { ensureAnimation(); if (!_animation->radial.animating()) { _animation->radial.start(dataProgress()); } } updateStatusText(); const auto radial = isRadialAnimation() || (streamedForWaiting && streamedForWaiting->waitingShown()); if (!bubble && !unwrapped) { Assert(rounding.has_value()); fillImageShadow(p, rthumb, *rounding, context); } const auto skipDrawingContent = context.skipDrawingParts == PaintContext::SkipDrawingParts::Content; if (streamed && !skipDrawingContent && !fullHiddenBySpoiler) { auto paused = context.paused; auto request = ::Media::Streaming::FrameRequest{ .outer = QSize(usew, painth) * cIntRetinaFactor(), .blurredBackground = true, }; if (isRound) { if (activeRoundStreamed()) { paused = false; } else { displayMute = true; } validateRoundingMask(request.outer); request.mask = _roundingMask; } else { request.rounding = MediaRoundingMask(rounding); } if (!activeRoundPlaying && activeOwnPlaying->instance.playerLocked()) { if (activeOwnPlaying->frozenFrame.isNull()) { activeOwnPlaying->frozenRequest = request; activeOwnPlaying->frozenFrame = streamed->frame(request); activeOwnPlaying->frozenStatusText = _statusText; } else if (activeOwnPlaying->frozenRequest != request) { activeOwnPlaying->frozenRequest = request; activeOwnPlaying->frozenFrame = streamed->frame(request); } p.drawImage(rthumb, activeOwnPlaying->frozenFrame); } else { if (activeOwnPlaying && !activeOwnPlaying->frozenFrame.isNull()) { activeOwnPlaying->frozenFrame = QImage(); activeOwnPlaying->frozenStatusText = QString(); } const auto frame = streamed->frameWithInfo(request); p.drawImage(rthumb, frame.image); if (!paused) { streamed->markFrameShown(); } } if (const auto playback = videoPlayback()) { const auto value = playback->value(); if (value > 0.) { auto pen = st->historyVideoMessageProgressFg()->p; auto was = p.pen(); pen.setWidth(st::radialLine); pen.setCapStyle(Qt::RoundCap); p.setPen(pen); p.setOpacity(st::historyVideoMessageProgressOpacity); auto from = arc::kQuarterLength; auto len = -qRound(arc::kFullLength * value); auto stepInside = st::radialLine / 2; { PainterHighQualityEnabler hq(p); p.drawArc(rthumb.marginsRemoved(QMargins(stepInside, stepInside, stepInside, stepInside)), from, len); } p.setPen(was); p.setOpacity(1.); } } } else if (!skipDrawingContent && !fullHiddenBySpoiler) { ensureDataMediaCreated(); validateThumbCache({ usew, painth }, isRound, rounding); p.drawImage(rthumb, _thumbCache); } if (!isRound && revealed < 1.) { p.setOpacity(1. - revealed); p.drawImage(rthumb.topLeft(), _spoiler->background); fillImageSpoiler(p, _spoiler.get(), rthumb, context); p.setOpacity(1.); } if (context.selected()) { if (isRound) { Ui::FillComplexEllipse(p, st, rthumb); } else { fillImageOverlay(p, rthumb, rounding, context); } } if (radial || (!streamingMode && ((!loaded && !_data->loading()) || !autoplay))) { const auto radialRevealed = 1.; const auto opacity = (item->isSending() || _data->uploading()) ? 1. : streamedForWaiting ? streamedForWaiting->waitingOpacity() : (radial && loaded) ? _animation->radial.opacity() : 1.; const auto radialOpacity = opacity * radialRevealed; const auto innerSize = st::msgFileLayout.thumbSize; auto inner = QRect(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()) { auto over = _animation->a_thumbOver.value(1.); p.setBrush(anim::brush(st->msgDateImgBg(), st->msgDateImgBgOver(), over)); } else { const auto over = ClickHandler::showAsActive( (_data->loading() || _data->uploading()) ? _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 = [&]() -> const style::icon * { if (streamingMode && !_data->uploading()) { return nullptr; } else if ((loaded || canBePlayed) && (!radial || cornerDownload)) { return &sti->historyFileThumbPlay; } else if (radial || _data->loading()) { if (!item->isSending() || _data->uploading()) { return &sti->historyFileThumbCancel; } return nullptr; } return &sti->historyFileThumbDownload; }(); if (icon) { icon->paintInCenter(p, inner); } p.setOpacity(radialRevealed); if (radial) { QRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine))); if (streamedForWaiting && !_data->uploading()) { Ui::InfiniteRadialAnimation::Draw( p, streamedForWaiting->waitingState(), rinner.topLeft(), rinner.size(), width(), sti->historyFileThumbRadialFg, st::msgFileRadialLine); } else if (!cornerDownload) { _animation->radial.draw( p, rinner, st::msgFileRadialLine, sti->historyFileThumbRadialFg); } } p.setOpacity(1.); } if (displayMute) { auto muteRect = style::rtlrect(rthumb.x() + (rthumb.width() - st::historyVideoMessageMuteSize) / 2, rthumb.y() + st::msgDateImgDelta, st::historyVideoMessageMuteSize, st::historyVideoMessageMuteSize, width()); p.setPen(Qt::NoPen); p.setBrush(sti->msgDateImgBg); PainterHighQualityEnabler hq(p); p.drawEllipse(muteRect); sti->historyVideoMessageMute.paintInCenter(p, muteRect); } const auto skipDrawingSurrounding = context.skipDrawingParts == PaintContext::SkipDrawingParts::Surrounding; if (!unwrapped && !skipDrawingSurrounding) { drawCornerStatus(p, context, QPoint()); } else if (!skipDrawingSurrounding) { if (isRound) { const auto mediaUnread = item->hasUnreadMediaFlag(); auto statusW = st::normalFont->width(_statusText) + 2 * st::msgDateImgPadding.x(); auto statusH = st::normalFont->height + 2 * st::msgDateImgPadding.y(); auto statusX = usex + paintx + st::msgDateImgDelta + st::msgDateImgPadding.x(); auto statusY = painty + painth - st::msgDateImgDelta - statusH + st::msgDateImgPadding.y(); if (mediaUnread) { statusW += st::mediaUnreadSkip + st::mediaUnreadSize; } Ui::FillRoundRect(p, style::rtlrect(statusX - st::msgDateImgPadding.x(), statusY - st::msgDateImgPadding.y(), statusW, statusH, width()), sti->msgServiceBg, sti->msgServiceBgCornersSmall); p.setFont(st::normalFont); p.setPen(st->msgServiceFg()); p.drawTextLeft(statusX, statusY, width(), _statusText, statusW - 2 * st::msgDateImgPadding.x()); if (mediaUnread) { p.setPen(Qt::NoPen); p.setBrush(st->msgServiceFg()); { PainterHighQualityEnabler hq(p); p.drawEllipse(style::rtlrect(statusX - st::msgDateImgPadding.x() + statusW - st::msgDateImgPadding.x() - st::mediaUnreadSize, statusY + st::mediaUnreadTop, st::mediaUnreadSize, st::mediaUnreadSize, width())); } } ensureTranscribeButton(); } if (via || reply || forwarded) { auto rectw = width() - usew - st::msgReplyPadding.left(); auto innerw = rectw - (st::msgReplyPadding.left() + st::msgReplyPadding.right()); auto recth = 0; auto forwardedHeightReal = forwarded ? forwarded->text.countHeight(innerw) : 0; auto forwardedHeight = qMin(forwardedHeightReal, kMaxGifForwardedBarLines * st::msgServiceNameFont->height); if (forwarded) { recth += st::msgReplyPadding.top() + forwardedHeight; } else if (via) { recth += st::msgReplyPadding.top() + st::msgServiceNameFont->height + (reply ? st::msgReplyPadding.top() : 0); } if (reply) { const auto replyMargins = reply->margins(); recth += reply->height() - ((forwarded || via) ? 0 : replyMargins.top()) - replyMargins.bottom(); } else { recth += st::msgReplyPadding.bottom(); } int rectx = rightAligned ? 0 : (usew + st::msgReplyPadding.left()); int recty = painty; if (rtl()) rectx = width() - rectx - rectw; Ui::FillRoundRect(p, rectx, recty, rectw, recth, sti->msgServiceBg, sti->msgServiceBgCornersSmall); p.setPen(st->msgServiceFg()); const auto textx = rectx + st::msgReplyPadding.left(); const auto textw = rectw - st::msgReplyPadding.left() - st::msgReplyPadding.right(); if (forwarded) { p.setTextPalette(st->serviceTextPalette()); auto breakEverywhere = (forwardedHeightReal > forwardedHeight); forwarded->text.drawElided(p, textx, recty + st::msgReplyPadding.top(), textw, kMaxGifForwardedBarLines, style::al_left, 0, -1, 0, breakEverywhere); p.restoreTextPalette(); const auto skip = std::min( forwarded->text.countHeight(textw), kMaxGifForwardedBarLines * st::msgServiceNameFont->height); recty += skip; } else if (via) { p.setFont(st::msgServiceNameFont); p.drawTextLeft(textx, recty + st::msgReplyPadding.top(), 2 * textx + textw, via->text); int skip = st::msgServiceNameFont->height + (reply ? st::msgReplyPadding.top() : 0); recty += skip; } if (reply) { if (forwarded || via) { recty += st::msgReplyPadding.top(); recth -= st::msgReplyPadding.top(); } else { recty -= reply->margins().top(); } reply->paint(p, _parent, context, rectx, recty, rectw, false); } } } if (!unwrapped && !_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; } auto highlightRequest = context.computeHighlightCache(); _caption.draw(p, { .position = QPoint(st::msgPadding.left(), top), .availableWidth = captionw, .palette = &stm->textPalette, .pre = stm->preCache.get(), .blockquote = context.quoteCache(parent()->colorIndex()), .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, .highlight = highlightRequest ? &*highlightRequest : nullptr, }); } else if (!inWebPage && !skipDrawingSurrounding) { auto fullRight = paintx + usex + usew; auto fullBottom = painty + painth; auto maxRight = _parent->width() - st::msgMargin.left(); if (_parent->hasFromPhoto()) { maxRight -= st::msgMargin.right(); } else { maxRight -= st::msgMargin.left(); } if (unwrapped && !rightAligned) { auto infoWidth = _parent->infoWidth(); // This is just some arbitrary point, // the main idea is to make info left aligned here. fullRight += infoWidth - st::normalFont->height; if (fullRight > maxRight) { fullRight = maxRight; } } if (isRound || needInfoDisplay()) { _parent->drawInfo( p, context, fullRight, fullBottom, 2 * paintx + paintw, (unwrapped ? InfoDisplayType::Background : InfoDisplayType::Image)); } if (const auto size = bubble ? std::nullopt : _parent->rightActionSize() ; size || (_transcribe && !rightAligned)) { const auto rightActionWidth = size ? size->width() : _transcribe->size().width(); auto fastShareLeft = (fullRight + st::historyFastShareLeft); auto fastShareTop = fullBottom - st::historyFastShareBottom - (size ? size->height() : 0); if (fastShareLeft + rightActionWidth > maxRight) { fastShareLeft = fullRight - rightActionWidth - st::msgDateImgDelta; fastShareTop -= st::msgDateImgDelta + st::msgDateImgPadding.y() + st::msgDateFont->height + st::msgDateImgPadding.y(); } if (size) { _parent->drawRightAction(p, context, fastShareLeft, fastShareTop, 2 * paintx + paintw); } if (_transcribe) { paintTranscribe(p, fastShareLeft, fastShareTop, true, context); } } if (rightAligned && _transcribe) { paintTranscribe(p, usex, fullBottom, false, context); } } } void Gif::paintTranscribe( Painter &p, int x, int y, bool right, const PaintContext &context) const { if (!_transcribe) { return; } const auto s = _transcribe->size(); _transcribe->paint( p, x - (right ? 0 : s.width()), y - s.height() - st::msgDateImgDelta, context); } void Gif::validateVideoThumbnail() const { const auto content = _dataMedia->videoThumbnailContent(); if (_videoThumbnailFrame || content.isEmpty()) { return; } auto info = v::get( ::Media::Clip::PrepareForSending(QString(), content).media); _videoThumbnailFrame = std::make_unique(info.thumbnail.isNull() ? Image::BlankMedia()->original() : info.thumbnail); } void Gif::validateThumbCache( QSize outer, bool isEllipse, std::optional rounding) const { const auto good = _dataMedia->goodThumbnail(); const auto normal = good ? good : _dataMedia->thumbnail(); if (!normal) { _data->loadThumbnail(_realParent->fullId()); validateVideoThumbnail(); } const auto videothumb = normal ? nullptr : _videoThumbnailFrame.get(); const auto blurred = normal ? (!good && (normal->width() < kUseNonBlurredThreshold) && (normal->height() < kUseNonBlurredThreshold)) : !videothumb; const auto ratio = style::DevicePixelRatio(); if (_thumbCache.size() == (outer * ratio) && _thumbCacheRounding == rounding && _thumbCacheBlurred == blurred && _thumbIsEllipse == isEllipse) { return; } auto cache = prepareThumbCache(outer); _thumbCache = isEllipse ? Images::Circle(std::move(cache)) : Images::Round(std::move(cache), MediaRoundingMask(rounding)); _thumbCacheRounding = rounding; _thumbCacheBlurred = blurred; } QImage Gif::prepareThumbCache(QSize outer) const { const auto good = _dataMedia->goodThumbnail(); const auto normal = good ? good : _dataMedia->thumbnail(); const auto videothumb = normal ? nullptr : _videoThumbnailFrame.get(); auto blurred = (!good && normal && (normal->width() < kUseNonBlurredThreshold) && (normal->height() < kUseNonBlurredThreshold)) ? normal : nullptr; const auto blurFromLarge = good || (normal && !blurred); const auto large = blurFromLarge ? normal : videothumb; if (videothumb) { } else if (const auto embedded = _dataMedia->thumbnailInline()) { blurred = embedded; } const auto resize = large ? ::Media::Streaming::DecideVideoFrameResize( outer, good ? large->size() : _data->dimensions) : ::Media::Streaming::ExpandDecision(); return PrepareWithBlurredBackground( outer, resize, large, blurFromLarge ? large : blurred); } void Gif::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; } const auto normal = _dataMedia->thumbnail(); auto container = std::optional(); const auto downscale = [&](Image *image) { if (!image || (image->width() <= 40 && image->height() <= 40)) { return image; } container.emplace(image->original().scaled( { 40, 40 }, Qt::KeepAspectRatio, Qt::SmoothTransformation)); return &*container; }; const auto embedded = _dataMedia->thumbnailInline(); const auto blurred = embedded ? embedded : downscale(normal); _spoiler->background = Images::Round( PrepareWithBlurredBackground( outer, ::Media::Streaming::ExpandDecision(), nullptr, blurred), MediaRoundingMask(rounding)); _spoiler->backgroundRounding = rounding; } void Gif::drawCornerStatus( Painter &p, const PaintContext &context, QPoint position) const { if (!needCornerStatusDisplay()) { return; } const auto own = activeOwnStreamed(); const auto st = context.st; const auto sti = context.imageStyle(); const auto text = (own && !own->frozenStatusText.isEmpty()) ? own->frozenStatusText : _statusText; const auto padding = st::msgDateImgPadding; const auto radial = _animation && _animation->radial.animating(); const auto cornerDownload = downloadInCorner() && !dataLoaded() && !_data->loadedInMediaCache(); const auto cornerMute = _streamed && _data->isVideoFile() && !cornerDownload; const auto addLeft = cornerDownload ? (st::historyVideoDownloadSize + 2 * padding.y()) : 0; const auto addRight = cornerMute ? st::historyVideoMuteSize : 0; const auto downloadWidth = cornerDownload ? st::normalFont->width(_downloadSize) : 0; const auto statusW = std::max(downloadWidth, st::normalFont->width(text)) + 2 * padding.x() + addLeft + addRight; const auto statusH = cornerDownload ? (st::historyVideoDownloadSize + 2 * padding.y()) : (st::normalFont->height + 2 * padding.y()); const auto statusX = position.x() + st::msgDateImgDelta + padding.x(); const auto statusY = position.y() + st::msgDateImgDelta + padding.y(); const auto around = style::rtlrect(statusX - padding.x(), statusY - padding.y(), statusW, statusH, width()); const auto statusTextTop = statusY + (cornerDownload ? (((statusH - 2 * st::normalFont->height) / 3) - padding.y()) : 0); Ui::FillRoundRect(p, around, sti->msgDateImgBg, sti->msgDateImgBgCorners); p.setFont(st::normalFont); p.setPen(st->msgDateImgFg()); p.drawTextLeft(statusX + addLeft, statusTextTop, width(), text, statusW - 2 * padding.x()); if (cornerDownload) { const auto downloadTextTop = statusY + st::normalFont->height + (2 * (statusH - 2 * st::normalFont->height) / 3) - padding.y(); p.drawTextLeft(statusX + addLeft, downloadTextTop, width(), _downloadSize, statusW - 2 * padding.x()); const auto inner = QRect(statusX + padding.y() - padding.x(), statusY, st::historyVideoDownloadSize, st::historyVideoDownloadSize); const auto &icon = _data->loading() ? sti->historyVideoCancel : sti->historyVideoDownload; icon.paintInCenter(p, inner); if (radial) { QRect rinner(inner.marginsRemoved(QMargins(st::historyVideoRadialLine, st::historyVideoRadialLine, st::historyVideoRadialLine, st::historyVideoRadialLine))); _animation->radial.draw(p, rinner, st::historyVideoRadialLine, sti->historyFileThumbRadialFg); } } else if (cornerMute) { sti->historyVideoMessageMute.paint(p, statusX - padding.x() - padding.y() + statusW - addRight, statusY - padding.y() + (statusH - st::historyVideoMessageMute.height()) / 2, width()); } } TextState Gif::cornerStatusTextState( QPoint point, StateRequest request, QPoint position) const { auto result = TextState(_parent); if (!needCornerStatusDisplay() || !downloadInCorner() || dataLoaded()) { return result; } const auto padding = st::msgDateImgPadding; const auto statusX = position.x() + st::msgDateImgDelta + padding.x(); const auto statusY = position.y() + st::msgDateImgDelta + padding.y(); const auto inner = QRect(statusX + padding.y() - padding.x(), statusY, st::historyVideoDownloadSize, st::historyVideoDownloadSize); if (inner.contains(point)) { result.link = _data->loading() ? _cancell : _savel; } return result; } TextState Gif::textState(QPoint point, StateRequest request) const { auto result = TextState(_parent); if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) { return result; } auto paintx = 0, painty = 0, paintw = width(), painth = height(); auto bubble = _parent->hasBubble(); if (bubble && !_caption.isEmpty()) { 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; } const auto outbg = _parent->hasOutLayout(); const auto inWebPage = (_parent->media() != this); const auto isRound = _data->isVideoMessage(); const auto unwrapped = isUnwrapped(); const auto item = _parent->data(); auto usew = paintw, usex = 0; const auto via = unwrapped ? item->Get() : nullptr; const auto reply = unwrapped ? _parent->displayedReply() : nullptr; const auto forwarded = unwrapped ? item->Get() : nullptr; const auto rightAligned = unwrapped && outbg && !_parent->delegate()->elementIsChatWide(); if (via || reply || forwarded) { usew = maxWidth() - additionalWidth(via, reply, forwarded); if (rightAligned) { usex = width() - usew; } } if (isRound) { accumulate_min(usew, painth); } if (rtl()) usex = width() - usex - usew; if (via || reply || forwarded) { auto rectw = paintw - usew - st::msgReplyPadding.left(); auto innerw = rectw - (st::msgReplyPadding.left() + st::msgReplyPadding.right()); auto recth = 0; auto forwardedHeightReal = forwarded ? forwarded->text.countHeight(innerw) : 0; auto forwardedHeight = qMin(forwardedHeightReal, kMaxGifForwardedBarLines * st::msgServiceNameFont->height); if (forwarded) { recth += st::msgReplyPadding.top() + forwardedHeight; } else if (via) { recth += st::msgReplyPadding.top() + st::msgServiceNameFont->height + (reply ? st::msgReplyPadding.top() : 0); } if (reply) { const auto replyMargins = reply->margins(); recth += reply->height() - ((forwarded || via) ? 0 : replyMargins.top()) - replyMargins.bottom(); } else { recth += st::msgReplyPadding.bottom(); } auto rectx = rightAligned ? 0 : (usew + st::msgReplyPadding.left()); auto recty = painty; if (rtl()) rectx = width() - rectx - rectw; if (forwarded) { if (QRect(rectx, recty, rectw, st::msgReplyPadding.top() + forwardedHeight).contains(point)) { auto breakEverywhere = (forwardedHeightReal > forwardedHeight); auto textRequest = request.forText(); if (breakEverywhere) { textRequest.flags |= Ui::Text::StateRequest::Flag::BreakEverywhere; } result = TextState(_parent, forwarded->text.getState( point - QPoint(rectx + st::msgReplyPadding.left(), recty + st::msgReplyPadding.top()), innerw, textRequest)); result.symbol = 0; result.afterSymbol = false; if (breakEverywhere) { result.cursor = CursorState::Forwarded; } else { result.cursor = CursorState::None; } return result; } recty += forwardedHeight; recth -= forwardedHeight; } else if (via) { auto viah = st::msgReplyPadding.top() + st::msgServiceNameFont->height + (reply ? 0 : st::msgReplyPadding.bottom()); if (QRect(rectx, recty, rectw, viah).contains(point)) { result.link = via->link; return result; } auto skip = st::msgServiceNameFont->height + (reply ? 2 * st::msgReplyPadding.top() : 0); recty += skip; recth -= skip; } if (reply) { if (forwarded || via) { recty += st::msgReplyPadding.top(); recth -= st::msgReplyPadding.top() + reply->margins().top(); } else { recty -= reply->margins().top(); } const auto replyRect = QRect(rectx, recty, rectw, recth); if (replyRect.contains(point)) { result.link = reply->link(); reply->ripple.lastPoint = point - replyRect.topLeft(); if (!reply->ripple.animation) { reply->ripple.animation = std::make_unique( st::defaultRippleAnimation, Ui::RippleAnimation::RoundRectMask( replyRect.size(), st::messageQuoteStyle.radius), [=] { item->history()->owner().requestItemRepaint(item); }); } return result; } } } if (!unwrapped) { if (const auto state = cornerStatusTextState(point, request, QPoint()); state.link) { return state; } } if (QRect(usex + paintx, painty, usew, painth).contains(point)) { ensureDataMediaCreated(); result.link = (_spoiler && !_spoiler->revealed) ? _spoiler->link : _data->uploading() ? _cancell : _realParent->isSending() ? nullptr : (dataLoaded() || _dataMedia->canBePlayed(_realParent)) ? _openl : _data->loading() ? _cancell : _savel; } if (unwrapped || _caption.isEmpty()) { auto fullRight = usex + paintx + usew; auto fullBottom = painty + painth; auto maxRight = _parent->width() - st::msgMargin.left(); if (_parent->hasFromPhoto()) { maxRight -= st::msgMargin.right(); } else { maxRight -= st::msgMargin.left(); } if (unwrapped && !rightAligned) { auto infoWidth = _parent->infoWidth(); // This is just some arbitrary point, // the main idea is to make info left aligned here. fullRight += infoWidth - st::normalFont->height; if (fullRight > maxRight) { fullRight = maxRight; } } if (!inWebPage) { const auto bottomInfoResult = _parent->bottomInfoTextState( fullRight, fullBottom, point, (unwrapped ? InfoDisplayType::Background : InfoDisplayType::Image)); if (bottomInfoResult.link || bottomInfoResult.cursor != CursorState::None || bottomInfoResult.customTooltip) { return bottomInfoResult; } } if (const auto size = bubble ? std::nullopt : _parent->rightActionSize()) { const auto rightActionWidth = size->width(); auto fastShareLeft = (fullRight + st::historyFastShareLeft); auto fastShareTop = fullBottom - st::historyFastShareBottom - size->height(); if (fastShareLeft + rightActionWidth > maxRight) { fastShareLeft = fullRight - rightActionWidth - st::msgDateImgDelta; fastShareTop -= st::msgDateImgDelta + st::msgDateImgPadding.y() + st::msgDateFont->height + st::msgDateImgPadding.y(); } if (QRect(QPoint(fastShareLeft, fastShareTop), *size).contains(point)) { result.link = _parent->rightActionLink(point - QPoint(fastShareLeft, fastShareTop)); } } if (_transcribe && _transcribe->contains(point)) { result.link = _transcribe->link(); } } return result; } void Gif::clickHandlerPressedChanged( const ClickHandlerPtr &handler, bool pressed) { File::clickHandlerPressedChanged(handler, pressed); if (!handler) { return; } else if (_transcribe && (handler == _transcribe->link())) { if (pressed) { _transcribe->addRipple([=] { repaint(); }); } else { _transcribe->stopRipple(); } } } TextForMimeData Gif::selectedText(TextSelection selection) const { return _caption.toTextForMimeData(selection); } TextWithEntities Gif::selectedQuote(TextSelection selection) const { return parent()->selectedQuote(_caption, selection); } TextSelection Gif::selectionFromQuote( const TextWithEntities "e) const { return parent()->selectionFromQuote(_caption, quote); } bool Gif::fullFeaturedGrouped(RectParts sides) const { return (sides & RectPart::Left) && (sides & RectPart::Right); } QSize Gif::sizeForGroupingOptimal(int maxWidth) const { return sizeForAspectRatio(); } QSize Gif::sizeForGrouping(int width) const { return sizeForAspectRatio(); } void Gif::drawGrouped( Painter &p, const PaintContext &context, const QRect &geometry, RectParts sides, Ui::BubbleRounding rounding, float64 highlightOpacity, not_null cacheKey, not_null cache) const { ensureDataMediaCreated(); const auto item = _parent->data(); const auto loaded = dataLoaded(); const auto displayLoading = item->isSending() || item->hasFailed() || _data->displayLoading(); const auto st = context.st; const auto sti = context.imageStyle(); const auto fullFeatured = fullFeaturedGrouped(sides); const auto cornerDownload = fullFeatured && downloadInCorner(); const auto canBePlayed = _dataMedia->canBePlayed(_realParent); const auto revealed = _spoiler ? _spoiler->revealAnimation.value(_spoiler->revealed ? 1. : 0.) : 1.; const auto fullHiddenBySpoiler = (revealed == 0.); if (revealed < 1.) { validateSpoilerImageCache(geometry.size(), rounding); } const auto autoplay = fullFeatured && autoplayEnabled() && canBePlayed && CanPlayInline(_data); const auto startPlay = autoplay && !_streamed; if (startPlay) { const_cast(this)->playAnimation(true); } else { checkStreamedIsStarted(); } const auto streamingMode = _streamed || autoplay; const auto activeOwnPlaying = activeOwnStreamed(); const auto streamed = activeOwnPlaying ? &activeOwnPlaying->instance : nullptr; const auto streamedForWaiting = _streamed ? &_streamed->instance : nullptr; if (displayLoading && (!streamedForWaiting || item->isSending() || _data->uploading() || (cornerDownload && _data->loading()))) { ensureAnimation(); if (!_animation->radial.animating()) { _animation->radial.start(dataProgress()); } } updateStatusText(); const auto radial = isRadialAnimation() || (streamedForWaiting && streamedForWaiting->waitingShown()); if (streamed && !fullHiddenBySpoiler) { const auto original = sizeForAspectRatio(); const auto originalWidth = style::ConvertScale(original.width()); const auto originalHeight = style::ConvertScale(original.height()); const auto pixSize = Ui::GetImageScaleSizeForGeometry( { originalWidth, originalHeight }, { geometry.width(), geometry.height() }); auto request = ::Media::Streaming::FrameRequest{ .resize = pixSize * cIntRetinaFactor(), .outer = geometry.size() * cIntRetinaFactor(), .rounding = MediaRoundingMask(rounding), }; if (activeOwnPlaying->instance.playerLocked()) { if (activeOwnPlaying->frozenFrame.isNull()) { activeOwnPlaying->frozenRequest = request; activeOwnPlaying->frozenFrame = streamed->frame(request); activeOwnPlaying->frozenStatusText = _statusText; } else if (activeOwnPlaying->frozenRequest != request) { activeOwnPlaying->frozenRequest = request; activeOwnPlaying->frozenFrame = streamed->frame(request); } p.drawImage(geometry, activeOwnPlaying->frozenFrame); } else { if (activeOwnPlaying) { activeOwnPlaying->frozenFrame = QImage(); activeOwnPlaying->frozenStatusText = QString(); } p.drawImage(geometry, streamed->frame(request)); if (!context.paused) { streamed->markFrameShown(); } } } else if (!fullHiddenBySpoiler) { validateGroupedCache(geometry, rounding, cacheKey, cache); p.drawPixmap(geometry, *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.); } if (radial || (!streamingMode && ((!loaded && !_data->loading()) || !autoplay))) { const auto radialRevealed = 1.; const auto opacity = (item->isSending() || _data->uploading()) ? 1. : streamedForWaiting ? streamedForWaiting->waitingOpacity() : (radial && loaded) ? _animation->radial.opacity() : 1.; const auto radialOpacity = opacity * radialRevealed; 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() || _data->uploading()) ? _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 = [&]() -> const style::icon * { if (_data->waitingForAlbum()) { return &sti->historyFileThumbWaiting; } else if (streamingMode && !_data->uploading()) { return nullptr; } else if ((loaded || canBePlayed) && (!radial || cornerDownload)) { return &sti->historyFileThumbPlay; } else if (radial || _data->loading()) { if (!item->isSending() || _data->uploading()) { return &sti->historyFileThumbCancel; } return nullptr; } return &sti->historyFileThumbDownload; }(); const auto previous = _data->waitingForAlbum() ? &sti->historyFileThumbCancel : nullptr; if (icon) { if (previous && radialOpacity > 0. && radialOpacity < 1.) { PaintInterpolatedIcon(p, *icon, *previous, radialOpacity, inner); } else { icon->paintInCenter(p, inner); } } p.setOpacity(radialRevealed); if (radial) { const auto line = st::historyGroupRadialLine; const auto rinner = inner.marginsRemoved({ line, line, line, line }); if (streamedForWaiting && !_data->uploading()) { Ui::InfiniteRadialAnimation::Draw( p, streamedForWaiting->waitingState(), rinner.topLeft(), rinner.size(), width(), sti->historyFileThumbRadialFg, st::msgFileRadialLine); } else if (!cornerDownload) { _animation->radial.draw( p, rinner, st::msgFileRadialLine, sti->historyFileThumbRadialFg); } } p.setOpacity(1.); } if (fullFeatured) { drawCornerStatus(p, context, geometry.topLeft()); } } TextState Gif::getStateGrouped( const QRect &geometry, RectParts sides, QPoint point, StateRequest request) const { if (!geometry.contains(point)) { return {}; } if (fullFeaturedGrouped(sides)) { if (const auto state = cornerStatusTextState(point, request, geometry.topLeft()); state.link) { return state; } } ensureDataMediaCreated(); return TextState(_parent, (_spoiler && !_spoiler->revealed) ? _spoiler->link : _data->uploading() ? _cancell : _realParent->isSending() ? nullptr : (dataLoaded() || _dataMedia->canBePlayed(_realParent)) ? _openl : _data->loading() ? _cancell : _savel); } void Gif::ensureDataMediaCreated() const { if (_dataMedia) { return; } _dataMedia = _data->createMediaView(); dataMediaCreated(); } void Gif::dataMediaCreated() const { Expects(_dataMedia != nullptr); _dataMedia->goodThumbnailWanted(); _dataMedia->thumbnailWanted(_realParent->fullId()); if (!autoplayEnabled()) { _dataMedia->videoThumbnailWanted(_realParent->fullId()); } history()->owner().registerHeavyViewPart(_parent); togglePollingStory(true); } void Gif::togglePollingStory(bool enabled) const { if (!_storyId || _pollingStory == enabled) { 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 = enabled; } bool Gif::uploading() const { return _data->uploading(); } void Gif::hideSpoilers() { _caption.setSpoilerRevealed(false, anim::type::instant); if (_spoiler) { _spoiler->revealed = false; } } bool Gif::needsBubble() const { if (_storyId) { return true; } else if (_data->isVideoMessage()) { return false; } else if (!_caption.isEmpty()) { return true; } const auto item = _parent->data(); return item->repliesAreComments() || item->externalReply() || item->viaBot() || _parent->displayedReply() || _parent->displayForwardedFrom() || _parent->displayFromName() || _parent->displayedTopicButton(); return false; } bool Gif::unwrapped() const { return isUnwrapped(); } QRect Gif::contentRectForReactions() const { if (!isUnwrapped()) { return QRect(0, 0, width(), height()); } auto paintx = 0, painty = 0, paintw = width(), painth = height(); auto usex = 0, usew = paintw; const auto outbg = _parent->hasOutLayout(); const auto rightAligned = outbg && !_parent->delegate()->elementIsChatWide(); const auto item = _parent->data(); const auto via = item->Get(); const auto reply = _parent->displayedReply(); const auto forwarded = item->Get(); if (via || reply || forwarded) { usew = maxWidth() - additionalWidth(via, reply, forwarded); } accumulate_max(usew, _parent->reactionsOptimalWidth()); if (rightAligned) { usex = width() - usew; } if (rtl()) usex = width() - usex - usew; return style::rtlrect(usex + paintx, painty, usew, painth, width()); } std::optional Gif::reactionButtonCenterOverride() const { if (!isUnwrapped()) { return std::nullopt; } const auto right = resolveCustomInfoRightBottom().x() - _parent->infoWidth() - 3 * st::msgDateImgPadding.x(); return right - st::reactionCornerSize.width() / 2; } QPoint Gif::resolveCustomInfoRightBottom() const { const auto inner = contentRectForReactions(); auto fullBottom = inner.y() + inner.height(); auto fullRight = inner.x() + inner.width(); const auto unwrapped = isUnwrapped(); if (unwrapped) { auto maxRight = _parent->width() - st::msgMargin.left(); if (_parent->hasFromPhoto()) { maxRight -= st::msgMargin.right(); } else { maxRight -= st::msgMargin.left(); } const auto infoWidth = _parent->infoWidth(); const auto outbg = _parent->hasOutLayout(); const auto rightAligned = outbg && !_parent->delegate()->elementIsChatWide(); if (!rightAligned) { // This is just some arbitrary point, // the main idea is to make info left aligned here. fullRight += infoWidth - st::normalFont->height; if (fullRight > maxRight) { fullRight = maxRight; } } } const auto skipx = unwrapped ? st::msgDateImgPadding.x() : (st::msgDateImgDelta + st::msgDateImgPadding.x()); const auto skipy = unwrapped ? st::msgDateImgPadding.y() : (st::msgDateImgDelta + st::msgDateImgPadding.y()); return QPoint(fullRight - skipx, fullBottom - skipy); } int Gif::additionalWidth() const { const auto item = _parent->data(); return additionalWidth( item->Get(), item->Get(), item->Get()); } bool Gif::isUnwrapped() const { return _data->isVideoMessage() && (_parent->media() == this); } void Gif::validateGroupedCache( const QRect &geometry, Ui::BubbleRounding rounding, not_null cacheKey, not_null cache) const { using Option = Images::Option; ensureDataMediaCreated(); const auto good = _dataMedia->goodThumbnail(); const auto thumb = _dataMedia->thumbnail(); const auto image = good ? good : thumb ? thumb : _dataMedia->thumbnailInline(); const auto blur = !good && (!thumb || (thumb->width() < kUseNonBlurredThreshold && thumb->height() < kUseNonBlurredThreshold)); const auto loadLevel = good ? 3 : thumb ? 2 : image ? 1 : 0; const auto width = geometry.width(); const auto height = geometry.height(); const auto options = (blur ? Option::Blur : Option(0)); const auto key = (uint64(width) << 48) | (uint64(height) << 32) | (uint64(options) << 16) | (uint64(rounding.key()) << 8) | (uint64(loadLevel)); if (*cacheKey == key) { return; } const auto original = sizeForAspectRatio(); const auto originalWidth = style::ConvertScale(original.width()); const auto originalHeight = style::ConvertScale(original.height()); const auto pixSize = Ui::GetImageScaleSizeForGeometry( { originalWidth, originalHeight }, { width, height }); const auto ratio = style::DevicePixelRatio(); *cacheKey = key; auto scaled = Images::Prepare( (image ? image : Image::BlankMedia().get())->original(), pixSize * ratio, { .options = options, .outer = { width, height } }); auto rounded = Images::Round( std::move(scaled), MediaRoundingMask(rounding)); *cache = Ui::PixmapFromImage(std::move(rounded)); } void Gif::setStatusSize(int64 newSize) const { if (newSize < 0) { _statusSize = newSize; _statusText = Ui::FormatDurationText(-newSize - 1); } else if (_data->isVideoMessage()) { _statusSize = newSize; _statusText = Ui::FormatDurationText(_data->duration() / 1000); } else { File::setStatusSize( newSize, _data->size, _data->isVideoFile() ? (_data->duration() / 1000) : -2, 0); } } void Gif::updateStatusText() const { ensureDataMediaCreated(); auto statusSize = int64(); if (_data->status == FileDownloadFailed || _data->status == FileUploadFailed) { statusSize = Ui::FileStatusSizeFailed; } else if (_data->uploading()) { statusSize = _data->uploadingData->offset; } else if (!downloadInCorner() && _data->loading()) { statusSize = _data->loadOffset(); } else if (dataLoaded() || _dataMedia->canBePlayed(_realParent)) { statusSize = Ui::FileStatusSizeLoaded; } else { statusSize = Ui::FileStatusSizeReady; } const auto round = activeRoundStreamed(); const auto own = activeOwnStreamed(); if (round || (own && own->frozenFrame.isNull() && _data->isVideoFile())) { const auto streamed = round ? round : &own->instance; const auto state = streamed->player().prepareLegacyState(); if (state.length) { auto position = int64(0); if (::Media::Player::IsStoppedAtEnd(state.state)) { position = state.length; } else if (!::Media::Player::IsStoppedOrStopping(state.state)) { position = state.position; } statusSize = -1 - int((state.length - position) / state.frequency + 1); } else { statusSize = -1 - (_data->duration() / 1000); } } if (statusSize != _statusSize) { setStatusSize(statusSize); } } QString Gif::additionalInfoString() const { if (_data->isVideoMessage()) { updateStatusText(); return _statusText; } return QString(); } bool Gif::isReadyForOpen() const { return true; } void Gif::parentTextUpdated() { if (_parent->media() == this) { refreshCaption(); history()->owner().requestViewResize(_parent); } } bool Gif::hasHeavyPart() const { return (_spoiler && _spoiler->animation) || _streamed || _dataMedia; } void Gif::unloadHeavyPart() { stopAnimation(); _dataMedia = nullptr; if (_spoiler) { _spoiler->background = _spoiler->cornerCache = QImage(); _spoiler->animation = nullptr; } _thumbCache = QImage(); _videoThumbnailFrame = nullptr; _caption.unloadPersistentAnimation(); togglePollingStory(false); } void Gif::refreshParentId(not_null realParent) { File::refreshParentId(realParent); if (_parent->media() == this) { refreshCaption(); } } void Gif::refreshCaption() { _caption = createCaption(_parent->data()); } int Gif::additionalWidth(const HistoryMessageVia *via, const HistoryMessageReply *reply, const HistoryMessageForwarded *forwarded) const { int result = 0; if (forwarded) { accumulate_max(result, st::msgReplyPadding.left() + st::msgReplyPadding.left() + forwarded->text.maxWidth() + st::msgReplyPadding.right()); } else if (via) { accumulate_max(result, st::msgReplyPadding.left() + st::msgReplyPadding.left() + via->maxWidth + st::msgReplyPadding.left()); } if (reply) { accumulate_max(result, st::msgReplyPadding.left() + reply->maxWidth()); } return result; } ::Media::Streaming::Instance *Gif::activeRoundStreamed() const { return ::Media::Player::instance()->roundVideoStreamed(_parent->data()); } Gif::Streamed *Gif::activeOwnStreamed() const { return (_streamed && _streamed->instance.player().ready() && !_streamed->instance.player().videoSize().isEmpty()) ? _streamed.get() : nullptr; } ::Media::Streaming::Instance *Gif::activeCurrentStreamed() const { if (const auto streamed = activeRoundStreamed()) { return streamed; } else if (const auto owned = activeOwnStreamed()) { return &owned->instance; } return nullptr; } ::Media::View::PlaybackProgress *Gif::videoPlayback() const { return ::Media::Player::instance()->roundVideoPlayback(_parent->data()); } void Gif::playAnimation(bool autoplay) { ensureDataMediaCreated(); if (_data->isVideoMessage() && !autoplay) { return; } else if (_streamed && autoplay) { return; } else if ((_streamed && autoplayEnabled()) || (!autoplay && _data->isVideoFile())) { _parent->delegate()->elementOpenDocument( _data, _parent->data()->fullId(), true); return; } if (_streamed) { stopAnimation(); } else if (_dataMedia->canBePlayed(_realParent)) { if (!autoplayEnabled()) { history()->owner().checkPlayingAnimations(); } createStreamedPlayer(); } } void Gif::createStreamedPlayer() { auto shared = _data->owner().streaming().sharedDocument( _data, _realParent->fullId()); if (!shared) { return; } setStreamed(std::make_unique( std::move(shared), [=] { repaintStreamedContent(); })); _streamed->instance.player().updates( ) | rpl::start_with_next_error([=](::Media::Streaming::Update &&update) { handleStreamingUpdate(std::move(update)); }, [=](::Media::Streaming::Error &&error) { handleStreamingError(std::move(error)); }, _streamed->instance.lifetime()); if (_streamed->instance.ready()) { streamingReady(base::duplicate(_streamed->instance.info())); } checkStreamedIsStarted(); } void Gif::startStreamedPlayer() const { Expects(_streamed != nullptr); auto options = ::Media::Streaming::PlaybackOptions(); options.audioId = AudioMsgId(_data, _realParent->fullId()); options.waitForMarkAsShown = true; //if (!_streamed->withSound) { options.mode = ::Media::Streaming::Mode::Video; options.loop = true; //} _streamed->instance.play(options); } void Gif::checkStreamedIsStarted() const { if (!_streamed || _streamed->instance.playerLocked()) { return; } else if (_streamed->instance.paused()) { _streamed->instance.resume(); } if (!_streamed->instance.active() && !_streamed->instance.failed()) { startStreamedPlayer(); } } void Gif::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 Gif::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 Gif::handleStreamingError(::Media::Streaming::Error &&error) { } void Gif::repaintStreamedContent() { const auto own = activeOwnStreamed(); if (own && !own->frozenFrame.isNull()) { return; } else if (_parent->delegate()->elementAnimationsPaused() && !activeRoundStreamed()) { return; } repaint(); } void Gif::streamingReady(::Media::Streaming::Information &&info) { if (info.video.size.width() * info.video.size.height() > kMaxInlineArea) { _data->dimensions = info.video.size; stopAnimation(); } else { history()->owner().requestViewResize(_parent); } } void Gif::stopAnimation() { if (_streamed) { setStreamed(nullptr); history()->owner().requestViewResize(_parent); } } void Gif::checkAnimation() { if (_streamed && !autoplayEnabled()) { stopAnimation(); } } float64 Gif::dataProgress() const { ensureDataMediaCreated(); return (_data->uploading() || (!_parent->data()->isSending() && !_parent->data()->hasFailed())) ? _dataMedia->progress() : 0; } bool Gif::dataFinished() const { return (!_parent->data()->isSending() && !_parent->data()->hasFailed()) ? (!_data->loading() && !_data->uploading()) : false; } bool Gif::dataLoaded() const { ensureDataMediaCreated(); return !_parent->data()->isSending() && !_parent->data()->hasFailed() && _dataMedia->loaded(); } bool Gif::needInfoDisplay() const { if (_parent->data()->isFakeBotAbout()) { return false; } return _parent->data()->isSending() || _data->uploading() || _parent->isUnderCursor() // Don't show the GIF badge if this message has text. || (!_parent->hasBubble() && _parent->isLastAndSelfMessage()); } bool Gif::needCornerStatusDisplay() const { return _data->isVideoFile() || needInfoDisplay(); } void Gif::ensureTranscribeButton() const { if (_data->isVideoMessage() && _data->session().premium()) { if (!_transcribe) { _transcribe = std::make_unique( _realParent, true); } } else { _transcribe = nullptr; } } } // namespace HistoryView