diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index d332394a72..6c2301ea84 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -465,7 +465,7 @@ Storage::SharedMediaTypesMask MediaFile::sharedMediaTypes() const { } bool MediaFile::canBeGrouped() const { - return _document->isVideoFile(); + return _document->isVideoFile() || _document->isSong(); } bool MediaFile::hasReplyPreview() const { diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 9121cf3f4b..ab556e3a9a 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -138,27 +138,101 @@ QString FastReplyText() { return tr::lng_fast_reply(tr::now); } -void PaintBubble(Painter &p, QRect rect, int outerWidth, bool selected, bool outbg, RectPart tailSide) { +void PaintBubble(Painter &p, QRect rect, int outerWidth, bool selected, bool outbg, RectPart tailSide, RectParts skip) { auto &bg = selected ? (outbg ? st::msgOutBgSelected : st::msgInBgSelected) : (outbg ? st::msgOutBg : st::msgInBg); - auto &sh = selected ? (outbg ? st::msgOutShadowSelected : st::msgInShadowSelected) : (outbg ? st::msgOutShadow : st::msgInShadow); + auto sh = &(selected ? (outbg ? st::msgOutShadowSelected : st::msgInShadowSelected) : (outbg ? st::msgOutShadow : st::msgInShadow)); auto cors = selected ? (outbg ? MessageOutSelectedCorners : MessageInSelectedCorners) : (outbg ? MessageOutCorners : MessageInCorners); - auto parts = RectPart::FullTop | RectPart::NoTopBottom | RectPart::Bottom; + auto parts = RectPart::None | RectPart::NoTopBottom; + if (skip & RectPart::Top) { + if (skip & RectPart::Bottom) { + p.fillRect(rect, bg); + return; + } + rect.setTop(rect.y() - st::historyMessageRadius); + } else { + parts |= RectPart::FullTop; + } + if (skip & RectPart::Bottom) { + rect.setHeight(rect.height() + st::historyMessageRadius); + sh = nullptr; + tailSide = RectPart::None; + } else { + parts |= RectPart::Bottom; + } if (tailSide == RectPart::Right) { parts |= RectPart::BottomLeft; p.fillRect(rect.x() + rect.width() - st::historyMessageRadius, rect.y() + rect.height() - st::historyMessageRadius, st::historyMessageRadius, st::historyMessageRadius, bg); auto &tail = selected ? st::historyBubbleTailOutRightSelected : st::historyBubbleTailOutRight; tail.paint(p, rect.x() + rect.width(), rect.y() + rect.height() - tail.height(), outerWidth); - p.fillRect(rect.x() + rect.width() - st::historyMessageRadius, rect.y() + rect.height(), st::historyMessageRadius + tail.width(), st::msgShadow, sh); + p.fillRect(rect.x() + rect.width() - st::historyMessageRadius, rect.y() + rect.height(), st::historyMessageRadius + tail.width(), st::msgShadow, *sh); } else if (tailSide == RectPart::Left) { parts |= RectPart::BottomRight; p.fillRect(rect.x(), rect.y() + rect.height() - st::historyMessageRadius, st::historyMessageRadius, st::historyMessageRadius, bg); auto &tail = selected ? (outbg ? st::historyBubbleTailOutLeftSelected : st::historyBubbleTailInLeftSelected) : (outbg ? st::historyBubbleTailOutLeft : st::historyBubbleTailInLeft); tail.paint(p, rect.x() - tail.width(), rect.y() + rect.height() - tail.height(), outerWidth); - p.fillRect(rect.x() - tail.width(), rect.y() + rect.height(), st::historyMessageRadius + tail.width(), st::msgShadow, sh); - } else { + p.fillRect(rect.x() - tail.width(), rect.y() + rect.height(), st::historyMessageRadius + tail.width(), st::msgShadow, *sh); + } else if (!(skip & RectPart::Bottom)) { parts |= RectPart::FullBottom; } - App::roundRect(p, rect, bg, cors, &sh, parts); + App::roundRect(p, rect, bg, cors, sh, parts); +} + +void PaintBubble(Painter &p, QRect rect, int outerWidth, bool selected, const std::vector &selection, bool outbg, RectPart tailSide) { + if (selection.empty()) { + PaintBubble( + p, + rect, + outerWidth, + selected, + outbg, + tailSide, + RectPart::None); + return; + } + const auto left = rect.x(); + const auto width = rect.width(); + const auto top = rect.y(); + const auto bottom = top + rect.height(); + auto from = top; + for (const auto &selected : selection) { + if (selected.top > from) { + const auto skip = RectPart::Bottom + | (from > top ? RectPart::Top : RectPart::None); + PaintBubble( + p, + QRect(left, from, width, selected.top - from), + outerWidth, + false, + outbg, + tailSide, + skip); + } + const auto skip = ((selected.top > top) + ? RectPart::Top + : RectPart::None) + | ((selected.top + selected.height < bottom) + ? RectPart::Bottom + : RectPart::None); + PaintBubble( + p, + QRect(left, selected.top, width, selected.height), + outerWidth, + true, + outbg, + tailSide, + skip); + from = selected.top + selected.height; + } + if (from < bottom) { + PaintBubble( + p, + QRect(left, from, width, bottom - from), + outerWidth, + false, + outbg, + tailSide, + RectPart::Top); + } } style::color FromNameFg(PeerId peerId, bool selected) { @@ -535,20 +609,52 @@ void Message::draw( auto entry = logEntryOriginal(); auto mediaDisplayed = media && media->isDisplayed(); + // Entry page is always a bubble bottom. + auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/); + auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); + + + auto mediaSelectionIntervals = (!selected && mediaDisplayed) + ? media->getBubbleSelectionIntervals(selection) + : std::vector(); + if (!mediaSelectionIntervals.empty()) { + auto localMediaBottom = g.top() + g.height(); + if (data()->repliesAreComments() || data()->externalReply()) { + localMediaBottom -= st::historyCommentsButtonHeight; + } + if (!mediaOnBottom) { + localMediaBottom -= st::msgPadding.bottom(); + } + if (entry) { + localMediaBottom -= entry->height(); + } + const auto localMediaTop = localMediaBottom - media->height(); + for (auto &[top, height] : mediaSelectionIntervals) { + top += localMediaTop; + } + } + auto skipTail = isAttachedToNext() || (media && media->skipBubbleTail()) || (keyboard != nullptr) || (context() == Context::Replies && data()->isDiscussionPost()); - auto displayTail = skipTail ? RectPart::None : (outbg && !Core::App().settings().chatWide()) ? RectPart::Right : RectPart::Left; - PaintBubble(p, g, width(), selected, outbg, displayTail); + auto displayTail = skipTail + ? RectPart::None + : (outbg && !Core::App().settings().chatWide()) + ? RectPart::Right + : RectPart::Left; + PaintBubble( + p, + g, + width(), + selected, + mediaSelectionIntervals, + outbg, + displayTail); auto inner = g; paintCommentsButton(p, inner, selected); - // Entry page is always a bubble bottom. - auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/); - auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop()); - auto trect = inner.marginsRemoved(st::msgPadding); if (mediaOnBottom) { trect.setHeight(trect.height() + st::msgPadding.bottom()); @@ -591,11 +697,16 @@ void Message::draw( ? !media->customInfoLayout() : true); if (needDrawInfo) { - drawInfo(p, inner.left() + inner.width(), inner.top() + inner.height(), 2 * inner.left() + inner.width(), selected, InfoDisplayType::Default); + const auto bottomSelected = selected + || (!mediaSelectionIntervals.empty() + && (mediaSelectionIntervals.back().top + + mediaSelectionIntervals.back().height + >= inner.y() + inner.height())); + drawInfo(p, inner.left() + inner.width(), inner.top() + inner.height(), 2 * inner.left() + inner.width(), bottomSelected, InfoDisplayType::Default); if (g != inner) { const auto o = p.opacity(); p.setOpacity(0.3); - const auto color = selected + const auto color = bottomSelected ? (outbg ? st::msgOutDateFgSelected : st::msgInDateFgSelected) : (outbg ? st::msgOutDateFg : st::msgInDateFg); p.fillRect(inner.left(), inner.top() + inner.height() - st::lineWidth, inner.width(), st::lineWidth, color); diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_document.cpp index 785998a76f..158f2c71e7 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_document.cpp @@ -247,8 +247,21 @@ QSize Document::countCurrentSize(int newWidth) { return { newWidth, newHeight }; } -void Document::draw(Painter &p, const QRect &r, TextSelection selection, crl::time ms) const { - if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return; +void Document::draw( + Painter &p, + const QRect &r, + TextSelection selection, + crl::time ms) const { + draw(p, width(), selection, ms, LayoutMode::Full); +} + +void Document::draw( + Painter &p, + int width, + TextSelection selection, + crl::time ms, + LayoutMode mode) const { + if (width < st::msgPadding.left() + st::msgPadding.right() + 1) return; ensureDataMediaCreated(); @@ -260,7 +273,7 @@ void Document::draw(Painter &p, const QRect &r, TextSelection selection, crl::ti bool loaded = dataLoaded(), displayLoading = _data->displayLoading(); bool selected = (selection == FullSelection); - int captionw = width() - st::msgPadding.left() - st::msgPadding.right(); + int captionw = width - st::msgPadding.left() - st::msgPadding.right(); auto outbg = _parent->hasOutLayout(); if (displayLoading) { @@ -284,7 +297,7 @@ void Document::draw(Painter &p, const QRect &r, TextSelection selection, crl::ti auto inWebPage = (_parent->media() != this); auto roundRadius = inWebPage ? ImageRoundRadius::Small : ImageRoundRadius::Large; - QRect rthumb(style::rtlrect(st::msgFileThumbPadding.left(), st::msgFileThumbPadding.top() - topMinus, st::msgFileThumbSize, st::msgFileThumbSize, width())); + QRect rthumb(style::rtlrect(st::msgFileThumbPadding.left(), st::msgFileThumbPadding.top() - topMinus, st::msgFileThumbSize, st::msgFileThumbSize, width)); QPixmap thumb; if (const auto normal = _dataMedia->thumbnail()) { thumb = normal->pixSingle(thumbed->_thumbw, 0, st::msgFileThumbSize, st::msgFileThumbSize, roundRadius); @@ -339,7 +352,7 @@ void Document::draw(Painter &p, const QRect &r, TextSelection selection, crl::ti bool over = ClickHandler::showAsActive(lnk); p.setFont(over ? st::semiboldFont->underline() : st::semiboldFont); p.setPen(outbg ? (selected ? st::msgFileThumbLinkOutFgSelected : st::msgFileThumbLinkOutFg) : (selected ? st::msgFileThumbLinkInFgSelected : st::msgFileThumbLinkInFg)); - p.drawTextLeft(nameleft, linktop, width(), thumbed->_link, thumbed->_linkw); + p.drawTextLeft(nameleft, linktop, width, thumbed->_link, thumbed->_linkw); } } else { nameleft = st::msgFilePadding.left() + st::msgFileSize + st::msgFilePadding.right(); @@ -348,7 +361,7 @@ void Document::draw(Painter &p, const QRect &r, TextSelection selection, crl::ti statustop = st::msgFileStatusTop - topMinus; bottom = st::msgFilePadding.top() + st::msgFileSize + st::msgFilePadding.bottom() - topMinus; - QRect inner(style::rtlrect(st::msgFilePadding.left(), st::msgFilePadding.top() - topMinus, st::msgFileSize, st::msgFileSize, width())); + QRect inner(style::rtlrect(st::msgFilePadding.left(), st::msgFilePadding.top() - topMinus, st::msgFileSize, st::msgFileSize, width)); p.setPen(Qt::NoPen); if (selected) { p.setBrush(outbg ? st::msgFileOutBgSelected : st::msgFileInBgSelected); @@ -386,7 +399,7 @@ void Document::draw(Painter &p, const QRect &r, TextSelection selection, crl::ti drawCornerDownload(p, selected); } - auto namewidth = width() - nameleft - nameright; + auto namewidth = width - nameleft - nameright; auto statuswidth = namewidth; auto voiceStatusOverride = QString(); @@ -470,9 +483,9 @@ void Document::draw(Painter &p, const QRect &r, TextSelection selection, crl::ti p.setFont(st::semiboldFont); p.setPen(outbg ? (selected ? st::historyFileNameOutFgSelected : st::historyFileNameOutFg) : (selected ? st::historyFileNameInFgSelected : st::historyFileNameInFg)); if (namewidth < named->_namew) { - p.drawTextLeft(nameleft, nametop, width(), st::semiboldFont->elided(named->_name, namewidth, Qt::ElideMiddle)); + p.drawTextLeft(nameleft, nametop, width, st::semiboldFont->elided(named->_name, namewidth, Qt::ElideMiddle)); } else { - p.drawTextLeft(nameleft, nametop, width(), named->_name, named->_namew); + p.drawTextLeft(nameleft, nametop, width, named->_name, named->_namew); } } @@ -480,7 +493,7 @@ void Document::draw(Painter &p, const QRect &r, TextSelection selection, crl::ti auto status = outbg ? (selected ? st::mediaOutFgSelected : st::mediaOutFg) : (selected ? st::mediaInFgSelected : st::mediaInFg); p.setFont(st::normalFont); p.setPen(status); - p.drawTextLeft(nameleft, statustop, width(), statusText); + p.drawTextLeft(nameleft, statustop, width, statusText); if (_parent->data()->hasUnreadMediaFlag()) { auto w = st::normalFont->width(statusText); @@ -490,14 +503,16 @@ void Document::draw(Painter &p, const QRect &r, TextSelection selection, crl::ti { PainterHighQualityEnabler hq(p); - p.drawEllipse(style::rtlrect(nameleft + w + st::mediaUnreadSkip, statustop + st::mediaUnreadTop, st::mediaUnreadSize, st::mediaUnreadSize, width())); + p.drawEllipse(style::rtlrect(nameleft + w + st::mediaUnreadSkip, statustop + st::mediaUnreadTop, st::mediaUnreadSize, st::mediaUnreadSize, width)); } } } - if (auto captioned = Get()) { - p.setPen(outbg ? (selected ? st::historyTextOutFgSelected : st::historyTextOutFg) : (selected ? st::historyTextInFgSelected : st::historyTextInFg)); - captioned->_caption.draw(p, st::msgPadding.left(), bottom, captionw, style::al_left, 0, -1, selection); + if (mode == LayoutMode::Full) { + if (auto captioned = Get()) { + p.setPen(outbg ? (selected ? st::historyTextOutFgSelected : st::historyTextOutFg) : (selected ? st::historyTextInFgSelected : st::historyTextInFg)); + captioned->_caption.draw(p, st::msgPadding.left(), bottom, captionw, style::al_left, 0, -1, selection); + } } } @@ -587,9 +602,20 @@ TextState Document::cornerDownloadTextState( } TextState Document::textState(QPoint point, StateRequest request) const { + return textState(point, { width(), height() }, request, LayoutMode::Full); +} + +TextState Document::textState( + QPoint point, + QSize layout, + StateRequest request, + LayoutMode mode) const { + const auto width = layout.width(); + const auto height = layout.height(); + auto result = TextState(_parent); - if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) { + if (width < st::msgPadding.left() + st::msgPadding.right() + 1) { return result; } @@ -607,14 +633,14 @@ TextState Document::textState(QPoint point, StateRequest request) const { linktop = st::msgFileThumbLinkTop - topMinus; bottom = st::msgFileThumbPadding.top() + st::msgFileThumbSize + st::msgFileThumbPadding.bottom() - topMinus; - QRect rthumb(style::rtlrect(st::msgFileThumbPadding.left(), st::msgFileThumbPadding.top() - topMinus, st::msgFileThumbSize, st::msgFileThumbSize, width())); + QRect rthumb(style::rtlrect(st::msgFileThumbPadding.left(), st::msgFileThumbPadding.top() - topMinus, st::msgFileThumbSize, st::msgFileThumbSize, width)); if ((_data->loading() || _data->uploading()) && rthumb.contains(point)) { result.link = _cancell; return result; } if (_data->status != FileUploadFailed) { - if (style::rtlrect(nameleft, linktop, thumbed->_linkw, st::semiboldFont->height, width()).contains(point)) { + if (style::rtlrect(nameleft, linktop, thumbed->_linkw, st::semiboldFont->height, width).contains(point)) { result.link = (_data->loading() || _data->uploading()) ? thumbed->_linkcancell : dataLoaded() @@ -632,7 +658,7 @@ TextState Document::textState(QPoint point, StateRequest request) const { if (const auto state = cornerDownloadTextState(point, request); state.link) { return state; } - QRect inner(style::rtlrect(st::msgFilePadding.left(), st::msgFilePadding.top() - topMinus, st::msgFileSize, st::msgFileSize, width())); + QRect inner(style::rtlrect(st::msgFilePadding.left(), st::msgFilePadding.top() - topMinus, st::msgFileSize, st::msgFileSize, width)); if ((_data->loading() || _data->uploading()) && inner.contains(point) && !downloadInCorner()) { result.link = _cancell; return result; @@ -640,7 +666,7 @@ TextState Document::textState(QPoint point, StateRequest request) const { } if (const auto voice = Get()) { - auto namewidth = width() - nameleft - nameright; + auto namewidth = width - nameleft - nameright; auto waveformbottom = st::msgFilePadding.top() - topMinus + st::msgWaveformMax + st::msgWaveformMin; if (QRect(nameleft, nametop, namewidth, waveformbottom - nametop).contains(point)) { const auto state = ::Media::Player::instance()->getState(AudioMsgId::Type::Voice); @@ -655,22 +681,24 @@ TextState Document::textState(QPoint point, StateRequest request) const { } } - auto painth = height(); - if (const auto captioned = Get()) { - if (point.y() >= bottom) { - result = TextState(_parent, captioned->_caption.getState( - point - QPoint(st::msgPadding.left(), bottom), - width() - st::msgPadding.left() - st::msgPadding.right(), - request.forText())); - return result; - } - auto captionw = width() - st::msgPadding.left() - st::msgPadding.right(); - painth -= captioned->_caption.countHeight(captionw); - if (isBubbleBottom()) { - painth -= st::msgPadding.bottom(); + auto painth = layout.height(); + if (mode == LayoutMode::Full) { + if (const auto captioned = Get()) { + if (point.y() >= bottom) { + result = TextState(_parent, captioned->_caption.getState( + point - QPoint(st::msgPadding.left(), bottom), + width - st::msgPadding.left() - st::msgPadding.right(), + request.forText())); + return result; + } + auto captionw = width - st::msgPadding.left() - st::msgPadding.right(); + painth -= captioned->_caption.countHeight(captionw); + if (isBubbleBottom()) { + painth -= st::msgPadding.bottom(); + } } } - if (QRect(0, 0, width(), painth).contains(point) + if (QRect(0, 0, width, painth).contains(point) && (!_data->loading() || downloadInCorner()) && !_data->uploading() && !_data->isNull()) { @@ -831,6 +859,37 @@ bool Document::hideForwardedFrom() const { return _data->isSong(); } +QSize Document::sizeForGrouping() const { + const auto height = st::msgFilePadding.top() + + st::msgFileSize + + st::msgFilePadding.bottom(); + return { maxWidth(), height }; +} + +void Document::drawGrouped( + Painter &p, + const QRect &clip, + TextSelection selection, + crl::time ms, + const QRect &geometry, + RectParts sides, + RectParts corners, + not_null cacheKey, + not_null cache) const { + p.translate(geometry.topLeft()); + draw(p, geometry.width(), selection, ms, LayoutMode::Grouped); + p.translate(-geometry.topLeft()); +} + +TextState Document::getStateGrouped( + const QRect &geometry, + RectParts sides, + QPoint point, + StateRequest request) const { + point -= geometry.topLeft(); + return textState(point, geometry.size(), request, LayoutMode::Grouped); +} + bool Document::voiceProgressAnimationCallback(crl::time now) { if (anim::Disabled()) { now += (2 * kAudioVoiceMsgUpdateView); diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.h b/Telegram/SourceFiles/history/view/media/history_view_document.h index c024d04c87..5f7dc99ec0 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_document.h +++ b/Telegram/SourceFiles/history/view/media/history_view_document.h @@ -61,6 +61,23 @@ public: QMargins bubbleMargins() const override; bool hideForwardedFrom() const override; + QSize sizeForGrouping() const override; + void drawGrouped( + Painter &p, + const QRect &clip, + TextSelection selection, + crl::time ms, + const QRect &geometry, + RectParts sides, + RectParts corners, + not_null cacheKey, + not_null cache) const override; + TextState getStateGrouped( + const QRect &geometry, + RectParts sides, + QPoint point, + StateRequest request) const override; + bool voiceProgressAnimationCallback(crl::time now); void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override; @@ -82,7 +99,22 @@ private: bool showPause = false; int realDuration = 0; }; + enum class LayoutMode { + Full, + Grouped, + }; + void draw( + Painter &p, + int width, + TextSelection selection, + crl::time ms, + LayoutMode mode) const; + [[nodiscard]] TextState textState( + QPoint point, + QSize layout, + StateRequest request, + LayoutMode mode) const; void ensureDataMediaCreated() const; [[nodiscard]] Ui::Text::String createCaption(); diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.h b/Telegram/SourceFiles/history/view/media/history_view_media.h index 98be501182..e0a2274307 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media.h @@ -45,6 +45,11 @@ enum class MediaInBubbleState { Bottom, }; +struct BubbleSelectionInterval { + int top = 0; + int height = 0; +}; + [[nodiscard]] QString DocumentTimestampLinkBase( not_null document, FullMsgId context); @@ -116,6 +121,12 @@ public: [[nodiscard]] TextSelection unskipSelection( TextSelection selection) const; + [[nodiscard]] virtual auto getBubbleSelectionIntervals( + TextSelection selection) const + -> std::vector { + return {}; + } + // if we press and drag this link should we drag the item [[nodiscard]] virtual bool dragItemByHandler( const ClickHandlerPtr &p) const = 0; diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp index f724353b1b..3e567220b7 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.cpp @@ -22,6 +22,32 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_history.h" namespace HistoryView { +namespace { + +std::vector LayoutPlaylist( + const std::vector &sizes) { + Expects(!sizes.empty()); + + auto result = std::vector(); + result.reserve(sizes.size()); + const auto width = ranges::max_element( + sizes, + std::less<>(), + &QSize::width)->width(); + auto top = 0; + for (const auto &size : sizes) { + result.push_back({ + .geometry = QRect(0, top, width, size.height()), + .sides = RectPart::Left | RectPart::Right + }); + top += size.height(); + } + result.front().sides |= RectPart::Top; + result.back().sides |= RectPart::Bottom; + return result; +} + +} // namespace GroupedMedia::Part::Part( not_null parent, @@ -39,7 +65,7 @@ GroupedMedia::GroupedMedia( const auto truncated = ranges::view::all( medias ) | ranges::view::transform([](const std::unique_ptr &v) { - return not_null(v.get()); + return v.get(); }) | ranges::view::take(kMaxSize); const auto result = applyGroup(truncated); @@ -66,6 +92,13 @@ GroupedMedia::~GroupedMedia() { base::take(_parts); } +GroupedMedia::Mode GroupedMedia::DetectMode(not_null media) { + const auto document = media->document(); + return (document && document->isSong()) + ? Mode::Playlist + : Mode::Grid; +} + QSize GroupedMedia::countOptimalSize() { if (_caption.hasSkipBlock()) { _caption.updateSkipBlock( @@ -81,11 +114,13 @@ QSize GroupedMedia::countOptimalSize() { sizes.push_back(media->sizeForGrouping()); } - const auto layout = Ui::LayoutMediaGroup( - sizes, - st::historyGroupWidthMax, - st::historyGroupWidthMin, - st::historyGroupSkip); + const auto layout = (_mode == Mode::Grid) + ? Ui::LayoutMediaGroup( + sizes, + st::historyGroupWidthMax, + st::historyGroupWidthMin, + st::historyGroupSkip) + : LayoutPlaylist(sizes); Assert(layout.size() == _parts.size()); auto maxWidth = 0; @@ -313,6 +348,33 @@ TextForMimeData GroupedMedia::selectedText( return _caption.toTextForMimeData(selection); } +auto GroupedMedia::getBubbleSelectionIntervals( + TextSelection selection) const +-> std::vector { + auto result = std::vector(); + for (auto i = 0, count = int(_parts.size()); i != count; ++i) { + const auto &part = _parts[i]; + if (!IsGroupItemSelection(selection, i)) { + continue; + } + const auto &geometry = part.geometry; + if (result.empty() + || (result.back().top + result.back().height + < geometry.top()) + || (result.back().top > geometry.top() + geometry.height())) { + result.push_back({ geometry.top(), geometry.height() }); + } else { + auto &last = result.back(); + const auto newTop = std::min(last.top, geometry.top()); + const auto newHeight = std::max( + last.top + last.height - newTop, + geometry.top() + geometry.height() - newTop); + last = BubbleSelectionInterval{ newTop, newHeight }; + } + } + return result; +} + void GroupedMedia::clickHandlerActiveChanged( const ClickHandlerPtr &p, bool active) { @@ -339,7 +401,15 @@ bool GroupedMedia::applyGroup(const DataMediaRange &medias) { return true; } + auto modeChosen = false; for (const auto media : medias) { + const auto mediaMode = DetectMode(media); + if (!modeChosen) { + _mode = mediaMode; + modeChosen = true; + } else if (mediaMode != _mode) { + continue; + } _parts.push_back(Part(_parent, media)); } if (_parts.empty()) { @@ -449,7 +519,7 @@ bool GroupedMedia::needsBubble() const { } bool GroupedMedia::computeNeedBubble() const { - if (!_caption.isEmpty()) { + if (!_caption.isEmpty() || _mode == Mode::Playlist) { return true; } if (const auto item = _parent->data()) { @@ -467,9 +537,10 @@ bool GroupedMedia::computeNeedBubble() const { } bool GroupedMedia::needInfoDisplay() const { - return (_parent->data()->id < 0 - || _parent->isUnderCursor() - || _parent->isLastAndSelfMessage()); + return (_mode != Mode::Playlist) + && (_parent->data()->id < 0 + || _parent->isUnderCursor() + || _parent->isLastAndSelfMessage()); } } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.h b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.h index 92e7f33aa2..3e1185c42a 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media_grouped.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media_grouped.h @@ -60,6 +60,9 @@ public: TextForMimeData selectedText(TextSelection selection) const override; + std::vector getBubbleSelectionIntervals( + TextSelection selection) const override; + void clickHandlerActiveChanged( const ClickHandlerPtr &p, bool active) override; @@ -76,12 +79,14 @@ public: HistoryMessageEdited *displayedEditBadge() const override; bool skipBubbleTail() const override { - return isRoundedInBubbleBottom() && _caption.isEmpty(); + return (_mode == Mode::Grid) + && isRoundedInBubbleBottom() + && _caption.isEmpty(); } void updateNeedBubbleState() override; bool needsBubble() const override; bool customInfoLayout() const override { - return _caption.isEmpty(); + return _caption.isEmpty() && (_mode != Mode::Playlist); } bool allowsFastShare() const override { return true; @@ -95,6 +100,10 @@ public: void parentTextUpdated() override; private: + enum class Mode : char { + Grid, + Playlist, + }; struct Part { Part( not_null parent, @@ -111,6 +120,8 @@ private: }; + [[nodiscard]] static Mode DetectMode(not_null media); + template bool applyGroup(const DataMediaRange &medias); @@ -131,6 +142,7 @@ private: Ui::Text::String _caption; std::vector _parts; + Mode _mode = Mode::Grid; bool _needBubble = false; };