1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "history/view/media/history_view_media_grouped.h"
9
10 #include "history/history_item_components.h"
11 #include "history/history_message.h"
12 #include "history/history.h"
13 #include "history/view/history_view_element.h"
14 #include "history/view/history_view_cursor_state.h"
15 #include "data/data_document.h"
16 #include "data/data_media_types.h"
17 #include "data/data_session.h"
18 #include "storage/storage_shared_media.h"
19 #include "lang/lang_keys.h"
20 #include "ui/grouped_layout.h"
21 #include "ui/chat/chat_style.h"
22 #include "ui/chat/message_bubble.h"
23 #include "ui/text/text_options.h"
24 #include "layout/layout_selection.h"
25 #include "styles/style_chat.h"
26
27 namespace HistoryView {
28 namespace {
29
LayoutPlaylist(const std::vector<QSize> & sizes)30 std::vector<Ui::GroupMediaLayout> LayoutPlaylist(
31 const std::vector<QSize> &sizes) {
32 Expects(!sizes.empty());
33
34 auto result = std::vector<Ui::GroupMediaLayout>();
35 result.reserve(sizes.size());
36 const auto width = ranges::max_element(
37 sizes,
38 std::less<>(),
39 &QSize::width)->width();
40 auto top = 0;
41 for (const auto &size : sizes) {
42 result.push_back({
43 .geometry = QRect(0, top, width, size.height()),
44 .sides = RectPart::Left | RectPart::Right
45 });
46 top += size.height();
47 }
48 result.front().sides |= RectPart::Top;
49 result.back().sides |= RectPart::Bottom;
50 return result;
51 }
52
53 } // namespace
54
Part(not_null<Element * > parent,not_null<Data::Media * > media)55 GroupedMedia::Part::Part(
56 not_null<Element*> parent,
57 not_null<Data::Media*> media)
58 : item(media->parent())
59 , content(media->createView(parent, item)) {
60 Assert(media->canBeGrouped());
61 }
62
GroupedMedia(not_null<Element * > parent,const std::vector<std::unique_ptr<Data::Media>> & medias)63 GroupedMedia::GroupedMedia(
64 not_null<Element*> parent,
65 const std::vector<std::unique_ptr<Data::Media>> &medias)
66 : Media(parent)
67 , _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) {
68 const auto truncated = ranges::views::all(
69 medias
70 ) | ranges::views::transform([](const std::unique_ptr<Data::Media> &v) {
71 return v.get();
72 }) | ranges::views::take(kMaxSize);
73 const auto result = applyGroup(truncated);
74
75 Ensures(result);
76 }
77
GroupedMedia(not_null<Element * > parent,const std::vector<not_null<HistoryItem * >> & items)78 GroupedMedia::GroupedMedia(
79 not_null<Element*> parent,
80 const std::vector<not_null<HistoryItem*>> &items)
81 : Media(parent)
82 , _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) {
83 const auto medias = ranges::views::all(
84 items
85 ) | ranges::views::transform([](not_null<HistoryItem*> item) {
86 return item->media();
87 }) | ranges::views::take(kMaxSize);
88 const auto result = applyGroup(medias);
89
90 Ensures(result);
91 }
92
~GroupedMedia()93 GroupedMedia::~GroupedMedia() {
94 // Destroy all parts while the media object is still not destroyed.
95 base::take(_parts);
96 }
97
DetectMode(not_null<Data::Media * > media)98 GroupedMedia::Mode GroupedMedia::DetectMode(not_null<Data::Media*> media) {
99 const auto document = media->document();
100 return (document && !document->isVideoFile())
101 ? Mode::Column
102 : Mode::Grid;
103 }
104
countOptimalSize()105 QSize GroupedMedia::countOptimalSize() {
106 if (_caption.hasSkipBlock()) {
107 _caption.updateSkipBlock(
108 _parent->skipBlockWidth(),
109 _parent->skipBlockHeight());
110 }
111
112 std::vector<QSize> sizes;
113 const auto partsCount = _parts.size();
114 sizes.reserve(partsCount);
115 auto maxWidth = 0;
116 if (_mode == Mode::Column) {
117 for (const auto &part : _parts) {
118 const auto &media = part.content;
119 media->initDimensions();
120 accumulate_max(maxWidth, media->maxWidth());
121 }
122 }
123 for (const auto &part : _parts) {
124 sizes.push_back(part.content->sizeForGroupingOptimal(maxWidth));
125 }
126
127 const auto layout = (_mode == Mode::Grid)
128 ? Ui::LayoutMediaGroup(
129 sizes,
130 st::historyGroupWidthMax,
131 st::historyGroupWidthMin,
132 st::historyGroupSkip)
133 : LayoutPlaylist(sizes);
134 Assert(layout.size() == _parts.size());
135
136 auto minHeight = 0;
137 for (auto i = 0, count = int(layout.size()); i != count; ++i) {
138 const auto &item = layout[i];
139 accumulate_max(maxWidth, item.geometry.x() + item.geometry.width());
140 accumulate_max(minHeight, item.geometry.y() + item.geometry.height());
141 _parts[i].initialGeometry = item.geometry;
142 _parts[i].sides = item.sides;
143 }
144
145 if (!_caption.isEmpty()) {
146 auto captionw = maxWidth - st::msgPadding.left() - st::msgPadding.right();
147 minHeight += st::mediaCaptionSkip + _caption.countHeight(captionw);
148 if (isBubbleBottom()) {
149 minHeight += st::msgPadding.bottom();
150 }
151 } else if (_mode == Mode::Column && _parts.back().item->emptyText()) {
152 const auto item = _parent->data();
153 const auto msgsigned = item->Get<HistoryMessageSigned>();
154 const auto views = item->Get<HistoryMessageViews>();
155 if ((msgsigned && !msgsigned->isAnonymousRank)
156 || (views
157 && (views->views.count >= 0 || views->replies.count > 0))
158 || displayedEditBadge()) {
159 minHeight += st::msgDateFont->height - st::msgDateDelta.y();
160 }
161 }
162
163 const auto groupPadding = groupedPadding();
164 minHeight += groupPadding.top() + groupPadding.bottom();
165
166 return { maxWidth, minHeight };
167 }
168
countCurrentSize(int newWidth)169 QSize GroupedMedia::countCurrentSize(int newWidth) {
170 accumulate_min(newWidth, maxWidth());
171 auto newHeight = 0;
172 if (_mode == Mode::Grid && newWidth < st::historyGroupWidthMin) {
173 return { newWidth, newHeight };
174 } else if (_mode == Mode::Column) {
175 auto top = 0;
176 for (auto &part : _parts) {
177 const auto size = part.content->sizeForGrouping(newWidth);
178 part.geometry = QRect(0, top, newWidth, size.height());
179 top += size.height();
180 }
181 newHeight = top;
182 } else {
183 const auto initialSpacing = st::historyGroupSkip;
184 const auto factor = newWidth / float64(maxWidth());
185 const auto scale = [&](int value) {
186 return int(base::SafeRound(value * factor));
187 };
188 const auto spacing = scale(initialSpacing);
189 for (auto &part : _parts) {
190 const auto sides = part.sides;
191 const auto initialGeometry = part.initialGeometry;
192 const auto needRightSkip = !(sides & RectPart::Right);
193 const auto needBottomSkip = !(sides & RectPart::Bottom);
194 const auto initialLeft = initialGeometry.x();
195 const auto initialTop = initialGeometry.y();
196 const auto initialRight = initialLeft
197 + initialGeometry.width()
198 + (needRightSkip ? initialSpacing : 0);
199 const auto initialBottom = initialTop
200 + initialGeometry.height()
201 + (needBottomSkip ? initialSpacing : 0);
202 const auto left = scale(initialLeft);
203 const auto top = scale(initialTop);
204 const auto width = scale(initialRight)
205 - left
206 - (needRightSkip ? spacing : 0);
207 const auto height = scale(initialBottom)
208 - top
209 - (needBottomSkip ? spacing : 0);
210 part.geometry = QRect(left, top, width, height);
211
212 accumulate_max(newHeight, top + height);
213 }
214 }
215 if (!_caption.isEmpty()) {
216 const auto captionw = newWidth - st::msgPadding.left() - st::msgPadding.right();
217 newHeight += st::mediaCaptionSkip + _caption.countHeight(captionw);
218 if (isBubbleBottom()) {
219 newHeight += st::msgPadding.bottom();
220 }
221 } else if (_mode == Mode::Column && _parts.back().item->emptyText()) {
222 const auto item = _parent->data();
223 const auto msgsigned = item->Get<HistoryMessageSigned>();
224 const auto views = item->Get<HistoryMessageViews>();
225 if ((msgsigned && !msgsigned->isAnonymousRank)
226 || (views
227 && (views->views.count >= 0 || views->replies.count > 0))
228 || displayedEditBadge()) {
229 newHeight += st::msgDateFont->height - st::msgDateDelta.y();
230 }
231 }
232
233 const auto groupPadding = groupedPadding();
234 newHeight += groupPadding.top() + groupPadding.bottom();
235
236 return { newWidth, newHeight };
237 }
238
refreshParentId(not_null<HistoryItem * > realParent)239 void GroupedMedia::refreshParentId(
240 not_null<HistoryItem*> realParent) {
241 for (const auto &part : _parts) {
242 part.content->refreshParentId(part.item);
243 }
244 }
245
cornersFromSides(RectParts sides) const246 RectParts GroupedMedia::cornersFromSides(RectParts sides) const {
247 auto result = Ui::GetCornersFromSides(sides);
248 if (!isBubbleTop()) {
249 result &= ~(RectPart::TopLeft | RectPart::TopRight);
250 }
251 if (!isRoundedInBubbleBottom() || !_caption.isEmpty()) {
252 result &= ~(RectPart::BottomLeft | RectPart::BottomRight);
253 }
254 return result;
255 }
256
groupedPadding() const257 QMargins GroupedMedia::groupedPadding() const {
258 if (_mode != Mode::Column) {
259 return QMargins();
260 }
261 const auto normal = st::msgFileLayout.padding;
262 const auto grouped = st::msgFileLayoutGrouped.padding;
263 const auto topMinus = isBubbleTop() ? 0 : st::msgFileTopMinus;
264 const auto lastHasCaption = isBubbleBottom()
265 && !_parts.back().item->emptyText();
266 const auto addToBottom = lastHasCaption ? st::msgPadding.bottom() : 0;
267 return QMargins(
268 0,
269 (normal.top() - grouped.top()) - topMinus,
270 0,
271 (normal.bottom() - grouped.bottom()) + addToBottom);
272 }
273
drawHighlight(Painter & p,const PaintContext & context,int top) const274 void GroupedMedia::drawHighlight(
275 Painter &p,
276 const PaintContext &context,
277 int top) const {
278 if (_mode != Mode::Column) {
279 return;
280 }
281 const auto skip = top + groupedPadding().top();
282 for (auto i = 0, count = int(_parts.size()); i != count; ++i) {
283 const auto &part = _parts[i];
284 const auto rect = part.geometry.translated(0, skip);
285 _parent->paintCustomHighlight(
286 p,
287 context,
288 rect.y(),
289 rect.height(),
290 part.item);
291 }
292 }
293
draw(Painter & p,const PaintContext & context) const294 void GroupedMedia::draw(Painter &p, const PaintContext &context) const {
295 auto wasCache = false;
296 auto nowCache = false;
297 const auto groupPadding = groupedPadding();
298 auto selection = context.selection;
299 const auto fullSelection = (selection == FullSelection);
300 const auto textSelection = (_mode == Mode::Column)
301 && !fullSelection
302 && !IsSubGroupSelection(selection);
303 for (auto i = 0, count = int(_parts.size()); i != count; ++i) {
304 const auto &part = _parts[i];
305 const auto partContext = context.withSelection(fullSelection
306 ? FullSelection
307 : textSelection
308 ? selection
309 : IsGroupItemSelection(selection, i)
310 ? FullSelection
311 : TextSelection());
312 if (textSelection) {
313 selection = part.content->skipSelection(selection);
314 }
315 const auto highlightOpacity = (_mode == Mode::Grid)
316 ? _parent->highlightOpacity(part.item)
317 : 0.;
318 if (!part.cache.isNull()) {
319 wasCache = true;
320 }
321 part.content->drawGrouped(
322 p,
323 partContext,
324 part.geometry.translated(0, groupPadding.top()),
325 part.sides,
326 cornersFromSides(part.sides),
327 highlightOpacity,
328 &part.cacheKey,
329 &part.cache);
330 if (!part.cache.isNull()) {
331 nowCache = true;
332 }
333 }
334 if (nowCache && !wasCache) {
335 history()->owner().registerHeavyViewPart(_parent);
336 }
337
338 // date
339 if (!_caption.isEmpty()) {
340 const auto captionw = width() - st::msgPadding.left() - st::msgPadding.right();
341 const auto captiony = height()
342 - groupPadding.bottom()
343 - (isBubbleBottom() ? st::msgPadding.bottom() : 0)
344 - _caption.countHeight(captionw);
345 const auto stm = context.messageStyle();
346 p.setPen(stm->historyTextFg);
347 _caption.draw(p, st::msgPadding.left(), captiony, captionw, style::al_left, 0, -1, selection);
348 } else if (_parent->media() == this) {
349 auto fullRight = width();
350 auto fullBottom = height();
351 if (needInfoDisplay()) {
352 _parent->drawInfo(
353 p,
354 context,
355 fullRight,
356 fullBottom,
357 width(),
358 InfoDisplayType::Image);
359 }
360 if (const auto size = _parent->hasBubble() ? std::nullopt : _parent->rightActionSize()) {
361 auto fastShareLeft = (fullRight + st::historyFastShareLeft);
362 auto fastShareTop = (fullBottom - st::historyFastShareBottom - size->height());
363 _parent->drawRightAction(p, context, fastShareLeft, fastShareTop, width());
364 }
365 }
366 }
367
getPartState(QPoint point,StateRequest request) const368 TextState GroupedMedia::getPartState(
369 QPoint point,
370 StateRequest request) const {
371 auto shift = 0;
372 for (const auto &part : _parts) {
373 if (part.geometry.contains(point)) {
374 auto result = part.content->getStateGrouped(
375 part.geometry,
376 part.sides,
377 point,
378 request);
379 result.symbol += shift;
380 result.itemId = part.item->fullId();
381 return result;
382 }
383 shift += part.content->fullSelectionLength();
384 }
385 return TextState(_parent->data());
386 }
387
pointState(QPoint point) const388 PointState GroupedMedia::pointState(QPoint point) const {
389 if (!QRect(0, 0, width(), height()).contains(point)) {
390 return PointState::Outside;
391 }
392 const auto groupPadding = groupedPadding();
393 point -= QPoint(0, groupPadding.top());
394 for (const auto &part : _parts) {
395 if (part.geometry.contains(point)) {
396 return PointState::GroupPart;
397 }
398 }
399 return PointState::Inside;
400 }
401
textState(QPoint point,StateRequest request) const402 TextState GroupedMedia::textState(QPoint point, StateRequest request) const {
403 const auto groupPadding = groupedPadding();
404 auto result = getPartState(point - QPoint(0, groupPadding.top()), request);
405 if (!result.link && !_caption.isEmpty()) {
406 const auto captionw = width() - st::msgPadding.left() - st::msgPadding.right();
407 const auto captiony = height()
408 - groupPadding.bottom()
409 - (isBubbleBottom() ? st::msgPadding.bottom() : 0)
410 - _caption.countHeight(captionw);
411 if (QRect(st::msgPadding.left(), captiony, captionw, height() - captiony).contains(point)) {
412 return TextState(
413 _captionItem
414 ? _captionItem
415 : _parent->data().get(),
416 _caption.getState(
417 point - QPoint(st::msgPadding.left(), captiony),
418 captionw,
419 request.forText()));
420 }
421 } else if (_parent->media() == this) {
422 auto fullRight = width();
423 auto fullBottom = height();
424 if (_parent->pointInTime(fullRight, fullBottom, point, InfoDisplayType::Image)) {
425 result.cursor = CursorState::Date;
426 }
427 if (const auto size = _parent->hasBubble() ? std::nullopt : _parent->rightActionSize()) {
428 auto fastShareLeft = (fullRight + st::historyFastShareLeft);
429 auto fastShareTop = (fullBottom - st::historyFastShareBottom - size->height());
430 if (QRect(fastShareLeft, fastShareTop, size->width(), size->height()).contains(point)) {
431 result.link = _parent->rightActionLink();
432 }
433 }
434 }
435 return result;
436 }
437
toggleSelectionByHandlerClick(const ClickHandlerPtr & p) const438 bool GroupedMedia::toggleSelectionByHandlerClick(
439 const ClickHandlerPtr &p) const {
440 for (const auto &part : _parts) {
441 if (part.content->toggleSelectionByHandlerClick(p)) {
442 return true;
443 }
444 }
445 return false;
446 }
447
dragItemByHandler(const ClickHandlerPtr & p) const448 bool GroupedMedia::dragItemByHandler(const ClickHandlerPtr &p) const {
449 for (const auto &part : _parts) {
450 if (part.content->dragItemByHandler(p)) {
451 return true;
452 }
453 }
454 return false;
455 }
456
adjustSelection(TextSelection selection,TextSelectType type) const457 TextSelection GroupedMedia::adjustSelection(
458 TextSelection selection,
459 TextSelectType type) const {
460 if (_mode != Mode::Column) {
461 return _caption.adjustSelection(selection, type);
462 }
463 auto checked = 0;
464 for (const auto &part : _parts) {
465 const auto modified = ShiftItemSelection(
466 part.content->adjustSelection(
467 UnshiftItemSelection(selection, checked),
468 type),
469 checked);
470 const auto till = checked + part.content->fullSelectionLength();
471 if (selection.from >= checked && selection.from < till) {
472 selection.from = modified.from;
473 }
474 if (selection.to <= till) {
475 selection.to = modified.to;
476 return selection;
477 }
478 }
479 return selection;
480 }
481
fullSelectionLength() const482 uint16 GroupedMedia::fullSelectionLength() const {
483 if (_mode != Mode::Column) {
484 return _caption.length();
485 }
486 auto result = 0;
487 for (const auto &part : _parts) {
488 result += part.content->fullSelectionLength();
489 }
490 return result;
491 }
492
hasTextForCopy() const493 bool GroupedMedia::hasTextForCopy() const {
494 if (_mode != Mode::Column) {
495 return !_caption.isEmpty();
496 }
497 for (const auto &part : _parts) {
498 if (part.content->hasTextForCopy()) {
499 return true;
500 }
501 }
502 return false;
503 }
504
selectedText(TextSelection selection) const505 TextForMimeData GroupedMedia::selectedText(
506 TextSelection selection) const {
507 if (_mode != Mode::Column) {
508 return _caption.toTextForMimeData(selection);
509 }
510 auto result = TextForMimeData();
511 for (const auto &part : _parts) {
512 auto text = part.content->selectedText(selection);
513 if (!text.empty()) {
514 if (result.empty()) {
515 result = std::move(text);
516 } else {
517 result.append(qstr("\n\n")).append(std::move(text));
518 }
519 }
520 selection = part.content->skipSelection(selection);
521 }
522 return result;
523 }
524
getBubbleSelectionIntervals(TextSelection selection) const525 auto GroupedMedia::getBubbleSelectionIntervals(
526 TextSelection selection) const
527 -> std::vector<Ui::BubbleSelectionInterval> {
528 auto result = std::vector<Ui::BubbleSelectionInterval>();
529 for (auto i = 0, count = int(_parts.size()); i != count; ++i) {
530 const auto &part = _parts[i];
531 if (!IsGroupItemSelection(selection, i)) {
532 continue;
533 }
534 const auto &geometry = part.geometry;
535 if (result.empty()
536 || (result.back().top + result.back().height
537 < geometry.top())
538 || (result.back().top > geometry.top() + geometry.height())) {
539 result.push_back({ geometry.top(), geometry.height() });
540 } else {
541 auto &last = result.back();
542 const auto newTop = std::min(last.top, geometry.top());
543 const auto newHeight = std::max(
544 last.top + last.height - newTop,
545 geometry.top() + geometry.height() - newTop);
546 last = Ui::BubbleSelectionInterval{ newTop, newHeight };
547 }
548 }
549 const auto groupPadding = groupedPadding();
550 for (auto &part : result) {
551 part.top += groupPadding.top();
552 }
553 if (IsGroupItemSelection(selection, 0)) {
554 result.front().top -= groupPadding.top();
555 result.front().height += groupPadding.top();
556 }
557 if (IsGroupItemSelection(selection, _parts.size() - 1)) {
558 result.back().height = height() - result.back().top;
559 }
560 return result;
561 }
562
clickHandlerActiveChanged(const ClickHandlerPtr & p,bool active)563 void GroupedMedia::clickHandlerActiveChanged(
564 const ClickHandlerPtr &p,
565 bool active) {
566 for (const auto &part : _parts) {
567 part.content->clickHandlerActiveChanged(p, active);
568 }
569 }
570
clickHandlerPressedChanged(const ClickHandlerPtr & p,bool pressed)571 void GroupedMedia::clickHandlerPressedChanged(
572 const ClickHandlerPtr &p,
573 bool pressed) {
574 for (const auto &part : _parts) {
575 part.content->clickHandlerPressedChanged(p, pressed);
576 if (pressed && part.content->dragItemByHandler(p)) {
577 // #TODO drag by item from album
578 // App::pressedLinkItem(part.view);
579 }
580 }
581 }
582
583 template <typename DataMediaRange>
applyGroup(const DataMediaRange & medias)584 bool GroupedMedia::applyGroup(const DataMediaRange &medias) {
585 if (validateGroupParts(medias)) {
586 return true;
587 }
588
589 auto modeChosen = false;
590 for (const auto media : medias) {
591 const auto mediaMode = DetectMode(media);
592 if (!modeChosen) {
593 _mode = mediaMode;
594 modeChosen = true;
595 } else if (mediaMode != _mode) {
596 continue;
597 }
598 _parts.push_back(Part(_parent, media));
599 }
600 if (_parts.empty()) {
601 return false;
602 }
603
604 Ensures(_parts.size() <= kMaxSize);
605 return true;
606 }
607
608 template <typename DataMediaRange>
validateGroupParts(const DataMediaRange & medias) const609 bool GroupedMedia::validateGroupParts(
610 const DataMediaRange &medias) const {
611 auto i = 0;
612 const auto count = _parts.size();
613 for (const auto media : medias) {
614 if (i >= count || _parts[i].item != media->parent()) {
615 return false;
616 }
617 ++i;
618 }
619 return (i == count);
620 }
621
main() const622 not_null<Media*> GroupedMedia::main() const {
623 Expects(!_parts.empty());
624
625 return _parts.back().content.get();
626 }
627
getCaption() const628 TextWithEntities GroupedMedia::getCaption() const {
629 return main()->getCaption();
630 }
631
sharedMediaTypes() const632 Storage::SharedMediaTypesMask GroupedMedia::sharedMediaTypes() const {
633 return main()->sharedMediaTypes();
634 }
635
getPhoto() const636 PhotoData *GroupedMedia::getPhoto() const {
637 return main()->getPhoto();
638 }
639
getDocument() const640 DocumentData *GroupedMedia::getDocument() const {
641 return main()->getDocument();
642 }
643
displayedEditBadge() const644 HistoryMessageEdited *GroupedMedia::displayedEditBadge() const {
645 for (const auto &part : _parts) {
646 if (!part.item->hideEditedBadge()) {
647 if (const auto edited = part.item->Get<HistoryMessageEdited>()) {
648 return edited;
649 }
650 }
651 }
652 return nullptr;
653 }
654
updateNeedBubbleState()655 void GroupedMedia::updateNeedBubbleState() {
656 using PartPtrOpt = std::optional<const Part*>;
657 const auto captionPart = [&]() -> PartPtrOpt {
658 if (_mode == Mode::Column) {
659 return std::nullopt;
660 }
661 auto result = PartPtrOpt();
662 for (const auto &part : _parts) {
663 if (!part.item->emptyText()) {
664 if (result) {
665 return std::nullopt;
666 } else {
667 result = ∂
668 }
669 }
670 }
671 return result;
672 }();
673 if (captionPart) {
674 const auto &part = (*captionPart);
675 struct Timestamp {
676 int duration = 0;
677 QString base;
678 };
679 const auto timestamp = [&]() -> Timestamp {
680 const auto &document = part->content->getDocument();
681 if (!document || document->isAnimation()) {
682 return {};
683 }
684 const auto duration = document->getDuration();
685 return {
686 .duration = duration,
687 .base = duration
688 ? DocumentTimestampLinkBase(
689 document,
690 part->item->fullId())
691 : QString(),
692 };
693 }();
694 _caption = createCaption(
695 part->item,
696 timestamp.duration,
697 timestamp.base);
698
699 _captionItem = part->item;
700 } else {
701 _captionItem = nullptr;
702 }
703 _needBubble = computeNeedBubble();
704 }
705
stopAnimation()706 void GroupedMedia::stopAnimation() {
707 for (const auto &part : _parts) {
708 part.content->stopAnimation();
709 }
710 }
711
checkAnimation()712 void GroupedMedia::checkAnimation() {
713 for (const auto &part : _parts) {
714 part.content->checkAnimation();
715 }
716 }
717
hasHeavyPart() const718 bool GroupedMedia::hasHeavyPart() const {
719 for (const auto &part : _parts) {
720 if (!part.cache.isNull() || part.content->hasHeavyPart()) {
721 return true;
722 }
723 }
724 return false;
725 }
726
unloadHeavyPart()727 void GroupedMedia::unloadHeavyPart() {
728 for (const auto &part : _parts) {
729 part.content->unloadHeavyPart();
730 part.cacheKey = 0;
731 part.cache = QPixmap();
732 }
733 }
734
parentTextUpdated()735 void GroupedMedia::parentTextUpdated() {
736 history()->owner().requestViewResize(_parent);
737 }
738
needsBubble() const739 bool GroupedMedia::needsBubble() const {
740 return _needBubble;
741 }
742
computeNeedBubble() const743 bool GroupedMedia::computeNeedBubble() const {
744 if (!_caption.isEmpty() || _mode == Mode::Column) {
745 return true;
746 }
747 if (const auto item = _parent->data()) {
748 if (item->repliesAreComments()
749 || item->externalReply()
750 || item->viaBot()
751 || _parent->displayedReply()
752 || _parent->displayForwardedFrom()
753 || _parent->displayFromName()
754 ) {
755 return true;
756 }
757 }
758 return false;
759 }
760
needInfoDisplay() const761 bool GroupedMedia::needInfoDisplay() const {
762 return (_mode != Mode::Column)
763 && (_parent->data()->isSending()
764 || _parent->data()->hasFailed()
765 || _parent->isUnderCursor()
766 || _parent->isLastAndSelfMessage());
767 }
768
769 } // namespace HistoryView
770