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 = &part;
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