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 "overview/overview_layout.h"
9 
10 #include "overview/overview_layout_delegate.h"
11 #include "data/data_document.h"
12 #include "data/data_document_resolver.h"
13 #include "data/data_session.h"
14 #include "data/data_web_page.h"
15 #include "data/data_media_types.h"
16 #include "data/data_peer.h"
17 #include "data/data_file_origin.h"
18 #include "data/data_photo_media.h"
19 #include "data/data_document_media.h"
20 #include "data/data_file_click_handler.h"
21 #include "styles/style_overview.h"
22 #include "styles/style_chat.h"
23 #include "core/file_utilities.h"
24 #include "boxes/add_contact_box.h"
25 #include "ui/boxes/confirm_box.h"
26 #include "lang/lang_keys.h"
27 #include "layout/layout_selection.h"
28 #include "mainwidget.h"
29 #include "storage/file_upload.h"
30 #include "mainwindow.h"
31 #include "main/main_session.h"
32 #include "media/audio/media_audio.h"
33 #include "media/player/media_player_instance.h"
34 #include "storage/localstorage.h"
35 #include "history/history.h"
36 #include "history/history_item.h"
37 #include "history/history_item_components.h"
38 #include "history/view/history_view_cursor_state.h"
39 #include "history/view/media/history_view_document.h" // DrawThumbnailAsSongCover
40 #include "base/unixtime.h"
41 #include "ui/effects/round_checkbox.h"
42 #include "ui/image/image.h"
43 #include "ui/text/format_song_document_name.h"
44 #include "ui/text/format_values.h"
45 #include "ui/text/text_options.h"
46 #include "ui/cached_round_corners.h"
47 #include "ui/ui_utility.h"
48 
49 namespace Overview {
50 namespace Layout {
51 namespace {
52 
53 using TextState = HistoryView::TextState;
54 
55 TextParseOptions _documentNameOptions = {
56 	TextParseMultiline | TextParseRichText | TextParseLinks | TextParseMarkdown, // flags
57 	0, // maxw
58 	0, // maxh
59 	Qt::LayoutDirectionAuto, // dir
60 };
61 
62 constexpr auto kMaxInlineArea = 1280 * 720;
63 
CanPlayInline(not_null<DocumentData * > document)64 [[nodiscard]] bool CanPlayInline(not_null<DocumentData*> document) {
65 	const auto dimensions = document->dimensions;
66 	return dimensions.width() * dimensions.height() <= kMaxInlineArea;
67 }
68 
69 
70 } // namespace
71 
72 class Checkbox {
73 public:
74 	template <typename UpdateCallback>
Checkbox(UpdateCallback callback,const style::RoundCheckbox & st)75 	Checkbox(UpdateCallback callback, const style::RoundCheckbox &st)
76 	: _updateCallback(callback)
77 	, _check(st, _updateCallback) {
78 	}
79 
80 	void paint(Painter &p, QPoint position, int outerWidth, bool selected, bool selecting);
81 
82 	void setActive(bool active);
83 	void setPressed(bool pressed);
84 
invalidateCache()85 	void invalidateCache() {
86 		_check.invalidateCache();
87 	}
88 
89 private:
90 	void startAnimation();
91 
92 	Fn<void()> _updateCallback;
93 	Ui::RoundCheckbox _check;
94 
95 	Ui::Animations::Simple _pression;
96 	bool _active = false;
97 	bool _pressed = false;
98 
99 };
100 
paint(Painter & p,QPoint position,int outerWidth,bool selected,bool selecting)101 void Checkbox::paint(Painter &p, QPoint position, int outerWidth, bool selected, bool selecting) {
102 	_check.setDisplayInactive(selecting);
103 	_check.setChecked(selected);
104 	const auto pression = _pression.value((_active && _pressed) ? 1. : 0.);
105 	const auto masterScale = 1. - (1. - st::overviewCheckPressedSize) * pression;
106 	_check.paint(p, position.x(), position.y(), outerWidth, masterScale);
107 }
108 
setActive(bool active)109 void Checkbox::setActive(bool active) {
110 	_active = active;
111 	if (_pressed) {
112 		startAnimation();
113 	}
114 }
115 
setPressed(bool pressed)116 void Checkbox::setPressed(bool pressed) {
117 	_pressed = pressed;
118 	if (_active) {
119 		startAnimation();
120 	}
121 }
122 
startAnimation()123 void Checkbox::startAnimation() {
124 	auto showPressed = (_pressed && _active);
125 	_pression.start(_updateCallback, showPressed ? 0. : 1., showPressed ? 1. : 0., st::overviewCheck.duration);
126 }
127 
ItemBase(not_null<Delegate * > delegate,not_null<HistoryItem * > parent)128 ItemBase::ItemBase(
129 	not_null<Delegate*> delegate,
130 	not_null<HistoryItem*> parent)
131 : _delegate(delegate)
132 , _parent(parent)
133 , _dateTime(ItemDateTime(parent)) {
134 }
135 
136 ItemBase::~ItemBase() = default;
137 
dateTime() const138 QDateTime ItemBase::dateTime() const {
139 	return _dateTime;
140 }
141 
clickHandlerActiveChanged(const ClickHandlerPtr & action,bool active)142 void ItemBase::clickHandlerActiveChanged(
143 		const ClickHandlerPtr &action,
144 		bool active) {
145 	_parent->history()->session().data().requestItemRepaint(_parent);
146 	if (_check) {
147 		_check->setActive(active);
148 	}
149 }
150 
clickHandlerPressedChanged(const ClickHandlerPtr & action,bool pressed)151 void ItemBase::clickHandlerPressedChanged(
152 		const ClickHandlerPtr &action,
153 		bool pressed) {
154 	_parent->history()->session().data().requestItemRepaint(_parent);
155 	if (_check) {
156 		_check->setPressed(pressed);
157 	}
158 }
159 
invalidateCache()160 void ItemBase::invalidateCache() {
161 	if (_check) {
162 		_check->invalidateCache();
163 	}
164 }
165 
paintCheckbox(Painter & p,QPoint position,bool selected,const PaintContext * context)166 void ItemBase::paintCheckbox(
167 		Painter &p,
168 		QPoint position,
169 		bool selected,
170 		const PaintContext *context) {
171 	if (selected || context->selecting) {
172 		ensureCheckboxCreated();
173 	}
174 	if (_check) {
175 		_check->paint(p, position, _width, selected, context->selecting);
176 	}
177 }
178 
checkboxStyle() const179 const style::RoundCheckbox &ItemBase::checkboxStyle() const {
180 	return st::overviewCheck;
181 }
182 
ensureCheckboxCreated()183 void ItemBase::ensureCheckboxCreated() {
184 	if (_check) {
185 		return;
186 	}
187 	const auto repaint = [=] {
188 		_parent->history()->session().data().requestItemRepaint(_parent);
189 	};
190 	_check = std::make_unique<Checkbox>(repaint, checkboxStyle());
191 }
192 
setDocumentLinks(not_null<DocumentData * > document,bool forceOpen)193 void RadialProgressItem::setDocumentLinks(
194 		not_null<DocumentData*> document,
195 		bool forceOpen) {
196 	const auto context = parent()->fullId();
197 	setLinks(
198 		std::make_shared<DocumentOpenClickHandler>(
199 			document,
200 			crl::guard(this, [=](FullMsgId id) {
201 				delegate()->openDocument(document, id, forceOpen);
202 			}),
203 			context),
204 		std::make_shared<DocumentSaveClickHandler>(document, context),
205 		std::make_shared<DocumentCancelClickHandler>(
206 			document,
207 			nullptr,
208 			context));
209 }
210 
clickHandlerActiveChanged(const ClickHandlerPtr & action,bool active)211 void RadialProgressItem::clickHandlerActiveChanged(
212 		const ClickHandlerPtr &action,
213 		bool active) {
214 	ItemBase::clickHandlerActiveChanged(action, active);
215 	if (action == _openl || action == _savel || action == _cancell) {
216 		if (iconAnimated()) {
217 			const auto repaint = [=] {
218 				parent()->history()->session().data().requestItemRepaint(
219 					parent());
220 			};
221 			_a_iconOver.start(
222 				repaint,
223 				active ? 0. : 1.,
224 				active ? 1. : 0.,
225 				st::msgFileOverDuration);
226 		}
227 	}
228 }
229 
setLinks(ClickHandlerPtr && openl,ClickHandlerPtr && savel,ClickHandlerPtr && cancell)230 void RadialProgressItem::setLinks(
231 		ClickHandlerPtr &&openl,
232 		ClickHandlerPtr &&savel,
233 		ClickHandlerPtr &&cancell) {
234 	_openl = std::move(openl);
235 	_savel = std::move(savel);
236 	_cancell = std::move(cancell);
237 }
238 
radialAnimationCallback(crl::time now) const239 void RadialProgressItem::radialAnimationCallback(crl::time now) const {
240 	const auto updated = [&] {
241 		return _radial->update(dataProgress(), dataFinished(), now);
242 	}();
243 	if (!anim::Disabled() || updated) {
244 		parent()->history()->session().data().requestItemRepaint(parent());
245 	}
246 	if (!_radial->animating()) {
247 		checkRadialFinished();
248 	}
249 }
250 
ensureRadial()251 void RadialProgressItem::ensureRadial() {
252 	if (_radial) {
253 		return;
254 	}
255 	_radial = std::make_unique<Ui::RadialAnimation>([=](crl::time now) {
256 		radialAnimationCallback(now);
257 	});
258 }
259 
checkRadialFinished() const260 void RadialProgressItem::checkRadialFinished() const {
261 	if (_radial && !_radial->animating() && dataLoaded()) {
262 		_radial.reset();
263 	}
264 }
265 
266 RadialProgressItem::~RadialProgressItem() = default;
267 
update(int newSize,int fullSize,int duration,crl::time realDuration)268 void StatusText::update(int newSize, int fullSize, int duration, crl::time realDuration) {
269 	setSize(newSize);
270 	if (_size == Ui::FileStatusSizeReady) {
271 		_text = (duration >= 0) ? Ui::FormatDurationAndSizeText(duration, fullSize) : (duration < -1 ? Ui::FormatGifAndSizeText(fullSize) : Ui::FormatSizeText(fullSize));
272 	} else if (_size == Ui::FileStatusSizeLoaded) {
273 		_text = (duration >= 0) ? Ui::FormatDurationText(duration) : (duration < -1 ? qsl("GIF") : Ui::FormatSizeText(fullSize));
274 	} else if (_size == Ui::FileStatusSizeFailed) {
275 		_text = tr::lng_attach_failed(tr::now);
276 	} else if (_size >= 0) {
277 		_text = Ui::FormatDownloadText(_size, fullSize);
278 	} else {
279 		_text = Ui::FormatPlayedText(-_size - 1, realDuration);
280 	}
281 }
282 
setSize(int newSize)283 void StatusText::setSize(int newSize) {
284 	_size = newSize;
285 }
286 
Photo(not_null<Delegate * > delegate,not_null<HistoryItem * > parent,not_null<PhotoData * > photo)287 Photo::Photo(
288 	not_null<Delegate*> delegate,
289 	not_null<HistoryItem*> parent,
290 	not_null<PhotoData*> photo)
291 : ItemBase(delegate, parent)
292 , _data(photo)
293 , _link(std::make_shared<PhotoOpenClickHandler>(
294 	photo,
295 	crl::guard(this, [=](FullMsgId id) { delegate->openPhoto(photo, id); }),
296 	parent->fullId())) {
297 	if (_data->inlineThumbnailBytes().isEmpty()
298 		&& (_data->hasExact(Data::PhotoSize::Small)
299 			|| _data->hasExact(Data::PhotoSize::Thumbnail))) {
300 		_data->load(Data::PhotoSize::Small, parent->fullId());
301 	}
302 }
303 
initDimensions()304 void Photo::initDimensions() {
305 	_maxw = 2 * st::overviewPhotoMinSize;
306 	_minh = _maxw;
307 }
308 
resizeGetHeight(int32 width)309 int32 Photo::resizeGetHeight(int32 width) {
310 	width = qMin(width, _maxw);
311 	if (width != _width || width != _height) {
312 		_width = qMin(width, _maxw);
313 		_height = _width;
314 	}
315 	return _height;
316 }
317 
paint(Painter & p,const QRect & clip,TextSelection selection,const PaintContext * context)318 void Photo::paint(Painter &p, const QRect &clip, TextSelection selection, const PaintContext *context) {
319 	const auto selected = (selection == FullSelection);
320 	const auto widthChanged = _pix.width() != _width * cIntRetinaFactor();
321 	if (!_goodLoaded || widthChanged) {
322 		ensureDataMediaCreated();
323 		const auto good = _dataMedia->loaded()
324 			|| (_dataMedia->image(Data::PhotoSize::Thumbnail) != nullptr);
325 		if ((good && !_goodLoaded) || widthChanged) {
326 			_goodLoaded = good;
327 			_pix = QPixmap();
328 			if (_goodLoaded) {
329 				setPixFrom(_dataMedia->image(Data::PhotoSize::Large)
330 					? _dataMedia->image(Data::PhotoSize::Large)
331 					: _dataMedia->image(Data::PhotoSize::Thumbnail));
332 			} else if (const auto small = _dataMedia->image(
333 					Data::PhotoSize::Small)) {
334 				setPixFrom(small);
335 			} else if (const auto blurred = _dataMedia->thumbnailInline()) {
336 				setPixFrom(blurred);
337 			}
338 		}
339 	}
340 
341 	if (_pix.isNull()) {
342 		p.fillRect(0, 0, _width, _height, st::overviewPhotoBg);
343 	} else {
344 		p.drawPixmap(0, 0, _pix);
345 	}
346 
347 	if (selected) {
348 		p.fillRect(0, 0, _width, _height, st::overviewPhotoSelectOverlay);
349 	}
350 	const auto checkDelta = st::overviewCheckSkip + st::overviewCheck.size;
351 	const auto checkLeft = _width - checkDelta;
352 	const auto checkTop = _height - checkDelta;
353 	paintCheckbox(p, { checkLeft, checkTop }, selected, context);
354 }
355 
setPixFrom(not_null<Image * > image)356 void Photo::setPixFrom(not_null<Image*> image) {
357 	const auto size = _width * cIntRetinaFactor();
358 	auto img = image->original();
359 	if (!_goodLoaded) {
360 		img = Images::prepareBlur(std::move(img));
361 	}
362 	if (img.width() == img.height()) {
363 		if (img.width() != size) {
364 			img = img.scaled(size, size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
365 		}
366 	} else if (img.width() > img.height()) {
367 		img = img.copy((img.width() - img.height()) / 2, 0, img.height(), img.height()).scaled(size, size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
368 	} else {
369 		img = img.copy(0, (img.height() - img.width()) / 2, img.width(), img.width()).scaled(size, size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
370 	}
371 	img.setDevicePixelRatio(cRetinaFactor());
372 
373 	// In case we have inline thumbnail we can unload all images and we still
374 	// won't get a blank image in the media viewer when the photo is opened.
375 	if (!_data->inlineThumbnailBytes().isEmpty()) {
376 		_dataMedia = nullptr;
377 		delegate()->unregisterHeavyItem(this);
378 	}
379 
380 	_pix = Ui::PixmapFromImage(std::move(img));
381 }
382 
ensureDataMediaCreated() const383 void Photo::ensureDataMediaCreated() const {
384 	if (_dataMedia) {
385 		return;
386 	}
387 	_dataMedia = _data->createMediaView();
388 	if (_data->inlineThumbnailBytes().isEmpty()) {
389 		_dataMedia->wanted(Data::PhotoSize::Small, parent()->fullId());
390 	}
391 	_dataMedia->wanted(Data::PhotoSize::Thumbnail, parent()->fullId());
392 	delegate()->registerHeavyItem(this);
393 }
394 
clearHeavyPart()395 void Photo::clearHeavyPart() {
396 	_dataMedia = nullptr;
397 }
398 
getState(QPoint point,StateRequest request) const399 TextState Photo::getState(
400 		QPoint point,
401 		StateRequest request) const {
402 	if (hasPoint(point)) {
403 		return { parent(), _link };
404 	}
405 	return {};
406 }
407 
Video(not_null<Delegate * > delegate,not_null<HistoryItem * > parent,not_null<DocumentData * > video)408 Video::Video(
409 	not_null<Delegate*> delegate,
410 	not_null<HistoryItem*> parent,
411 	not_null<DocumentData*> video)
412 : RadialProgressItem(delegate, parent)
413 , _data(video)
414 , _duration(Ui::FormatDurationText(_data->getDuration())) {
415 	setDocumentLinks(_data);
416 	_data->loadThumbnail(parent->fullId());
417 }
418 
419 Video::~Video() = default;
420 
initDimensions()421 void Video::initDimensions() {
422 	_maxw = 2 * st::overviewPhotoMinSize;
423 	_minh = _maxw;
424 }
425 
resizeGetHeight(int32 width)426 int32 Video::resizeGetHeight(int32 width) {
427 	_width = qMin(width, _maxw);
428 	_height = _width;
429 	return _height;
430 }
431 
paint(Painter & p,const QRect & clip,TextSelection selection,const PaintContext * context)432 void Video::paint(Painter &p, const QRect &clip, TextSelection selection, const PaintContext *context) {
433 	ensureDataMediaCreated();
434 
435 	const auto selected = (selection == FullSelection);
436 	const auto blurred = _dataMedia->thumbnailInline();
437 	const auto thumbnail = _dataMedia->thumbnail();
438 	const auto good = _dataMedia->goodThumbnail();
439 
440 	bool loaded = dataLoaded(), displayLoading = _data->displayLoading();
441 	if (displayLoading) {
442 		ensureRadial();
443 		if (!_radial->animating()) {
444 			_radial->start(dataProgress());
445 		}
446 	}
447 	updateStatusText();
448 	const auto radial = isRadialAnimation();
449 	const auto radialOpacity = radial ? _radial->opacity() : 0.;
450 
451 	if ((blurred || thumbnail || good)
452 		&& ((_pix.width() != _width * cIntRetinaFactor())
453 			|| (_pixBlurred && (thumbnail || good)))) {
454 		auto size = _width * cIntRetinaFactor();
455 		auto img = good
456 			? good->original()
457 			: thumbnail
458 			? thumbnail->original()
459 			: Images::prepareBlur(blurred->original());
460 		if (img.width() == img.height()) {
461 			if (img.width() != size) {
462 				img = img.scaled(size, size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
463 			}
464 		} else if (img.width() > img.height()) {
465 			img = img.copy((img.width() - img.height()) / 2, 0, img.height(), img.height()).scaled(size, size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
466 		} else {
467 			img = img.copy(0, (img.height() - img.width()) / 2, img.width(), img.width()).scaled(size, size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
468 		}
469 		img.setDevicePixelRatio(cRetinaFactor());
470 
471 		_pix = Ui::PixmapFromImage(std::move(img));
472 		_pixBlurred = !(thumbnail || good);
473 	}
474 
475 	if (_pix.isNull()) {
476 		p.fillRect(0, 0, _width, _height, st::overviewPhotoBg);
477 	} else {
478 		p.drawPixmap(0, 0, _pix);
479 	}
480 
481 	if (selected) {
482 		p.fillRect(QRect(0, 0, _width, _height), st::overviewPhotoSelectOverlay);
483 	}
484 
485 	if (!selected && !context->selecting && radialOpacity < 1.) {
486 		if (clip.intersects(QRect(0, _height - st::normalFont->height, _width, st::normalFont->height))) {
487 			const auto download = !loaded && !_dataMedia->canBePlayed();
488 			const auto &icon = download
489 				? (selected ? st::overviewVideoDownloadSelected : st::overviewVideoDownload)
490 				: (selected ? st::overviewVideoPlaySelected : st::overviewVideoPlay);
491 			const auto text = download ? _status.text() : _duration;
492 			const auto margin = st::overviewVideoStatusMargin;
493 			const auto padding = st::overviewVideoStatusPadding;
494 			const auto statusX = margin + padding.x(), statusY = _height - margin - padding.y() - st::normalFont->height;
495 			const auto statusW = icon.width() + padding.x() + st::normalFont->width(text) + 2 * padding.x();
496 			const auto statusH = st::normalFont->height + 2 * padding.y();
497 			p.setOpacity(1. - radialOpacity);
498 			Ui::FillRoundRect(p, statusX - padding.x(), statusY - padding.y(), statusW, statusH, selected ? st::msgDateImgBgSelected : st::msgDateImgBg, selected ? Ui::OverviewVideoSelectedCorners : Ui::OverviewVideoCorners);
499 			p.setFont(st::normalFont);
500 			p.setPen(st::msgDateImgFg);
501 			icon.paint(p, statusX, statusY + (st::normalFont->height - icon.height()) / 2, _width);
502 			p.drawTextLeft(statusX + icon.width() + padding.x(), statusY, _width, text, statusW - 2 * padding.x());
503 		}
504 	}
505 
506 	QRect inner((_width - st::overviewVideoRadialSize) / 2, (_height - st::overviewVideoRadialSize) / 2, st::overviewVideoRadialSize, st::overviewVideoRadialSize);
507 	if (radial && clip.intersects(inner)) {
508 		p.setOpacity(radialOpacity);
509 		p.setPen(Qt::NoPen);
510 		if (selected) {
511 			p.setBrush(st::msgDateImgBgSelected);
512 		} else {
513 			auto over = ClickHandler::showAsActive((_data->loading() || _data->uploading()) ? _cancell : (loaded || _dataMedia->canBePlayed()) ? _openl : _savel);
514 			p.setBrush(anim::brush(st::msgDateImgBg, st::msgDateImgBgOver, _a_iconOver.value(over ? 1. : 0.)));
515 		}
516 
517 		{
518 			PainterHighQualityEnabler hq(p);
519 			p.drawEllipse(inner);
520 		}
521 
522 		const auto icon = [&] {
523 			return &(selected ? st::historyFileThumbCancelSelected : st::historyFileThumbCancel);
524 		}();
525 		icon->paintInCenter(p, inner);
526 		if (radial) {
527 			p.setOpacity(1);
528 			QRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine)));
529 			_radial->draw(p, rinner, st::msgFileRadialLine, selected ? st::historyFileThumbRadialFgSelected : st::historyFileThumbRadialFg);
530 		}
531 	}
532 	p.setOpacity(1);
533 
534 	const auto checkDelta = st::overviewCheckSkip + st::overviewCheck.size;
535 	const auto checkLeft = _width - checkDelta;
536 	const auto checkTop = _height - checkDelta;
537 	paintCheckbox(p, { checkLeft, checkTop }, selected, context);
538 }
539 
ensureDataMediaCreated() const540 void Video::ensureDataMediaCreated() const {
541 	if (_dataMedia) {
542 		return;
543 	}
544 	_dataMedia = _data->createMediaView();
545 	_dataMedia->goodThumbnailWanted();
546 	_dataMedia->thumbnailWanted(parent()->fullId());
547 	delegate()->registerHeavyItem(this);
548 }
549 
clearHeavyPart()550 void Video::clearHeavyPart() {
551 	_dataMedia = nullptr;
552 }
553 
dataProgress() const554 float64 Video::dataProgress() const {
555 	ensureDataMediaCreated();
556 	return _dataMedia->progress();
557 }
558 
dataFinished() const559 bool Video::dataFinished() const {
560 	return !_data->loading();
561 }
562 
dataLoaded() const563 bool Video::dataLoaded() const {
564 	ensureDataMediaCreated();
565 	return _dataMedia->loaded();
566 }
567 
iconAnimated() const568 bool Video::iconAnimated() const {
569 	return true;
570 }
571 
getState(QPoint point,StateRequest request) const572 TextState Video::getState(
573 		QPoint point,
574 		StateRequest request) const {
575 	if (hasPoint(point)) {
576 		ensureDataMediaCreated();
577 		const auto link = (_data->loading() || _data->uploading())
578 			? _cancell
579 			: (dataLoaded() || _dataMedia->canBePlayed())
580 			? _openl
581 			: _savel;
582 		return { parent(), link };
583 	}
584 	return {};
585 }
586 
updateStatusText()587 void Video::updateStatusText() {
588 	int statusSize = 0;
589 	if (_data->status == FileDownloadFailed || _data->status == FileUploadFailed) {
590 		statusSize = Ui::FileStatusSizeFailed;
591 	} else if (_data->uploading()) {
592 		statusSize = _data->uploadingData->offset;
593 	} else if (dataLoaded()) {
594 		statusSize = Ui::FileStatusSizeLoaded;
595 	} else {
596 		statusSize = Ui::FileStatusSizeReady;
597 	}
598 	if (statusSize != _status.size()) {
599 		int status = statusSize, size = _data->size;
600 		if (statusSize >= 0 && statusSize < 0x7F000000) {
601 			size = status;
602 			status = Ui::FileStatusSizeReady;
603 		}
604 		_status.update(status, size, -1, 0);
605 		_status.setSize(statusSize);
606 	}
607 }
608 
Voice(not_null<Delegate * > delegate,not_null<HistoryItem * > parent,not_null<DocumentData * > voice,const style::OverviewFileLayout & st)609 Voice::Voice(
610 	not_null<Delegate*> delegate,
611 	not_null<HistoryItem*> parent,
612 	not_null<DocumentData*> voice,
613 	const style::OverviewFileLayout &st)
614 : RadialProgressItem(delegate, parent)
615 , _data(voice)
616 , _namel(std::make_shared<DocumentOpenClickHandler>(
617 	_data,
618 	crl::guard(this, [=](FullMsgId id) {
619 		delegate->openDocument(_data, id);
620 	}),
621 	parent->fullId()))
622 , _st(st) {
623 	AddComponents(Info::Bit());
624 
625 	setDocumentLinks(_data);
626 	_data->loadThumbnail(parent->fullId());
627 
628 	updateName();
629 	const auto dateText = textcmdLink(
630 		1,
631 		TextUtilities::EscapeForRichParsing(
632 			langDateTime(base::unixtime::parse(_data->date))));
633 	TextParseOptions opts = { TextParseRichText, 0, 0, Qt::LayoutDirectionAuto };
634 	_details.setText(
635 		st::defaultTextStyle,
636 		tr::lng_date_and_duration(
637 			tr::now,
638 			lt_date,
639 			dateText,
640 			lt_duration,
641 			Ui::FormatDurationText(duration())),
642 		opts);
643 	_details.setLink(1, goToMessageClickHandler(parent));
644 }
645 
initDimensions()646 void Voice::initDimensions() {
647 	_maxw = _st.maxWidth;
648 	_minh = _st.songPadding.top() + _st.songThumbSize + _st.songPadding.bottom() + st::lineWidth;
649 }
650 
paint(Painter & p,const QRect & clip,TextSelection selection,const PaintContext * context)651 void Voice::paint(Painter &p, const QRect &clip, TextSelection selection, const PaintContext *context) {
652 	ensureDataMediaCreated();
653 	bool selected = (selection == FullSelection);
654 	bool loaded = dataLoaded(), displayLoading = _data->displayLoading();
655 
656 	if (displayLoading) {
657 		ensureRadial();
658 		if (!_radial->animating()) {
659 			_radial->start(dataProgress());
660 		}
661 	}
662 	const auto showPause = updateStatusText();
663 	const auto nameVersion = parent()->fromOriginal()->nameVersion;
664 	if (nameVersion > _nameVersion) {
665 		updateName();
666 	}
667 	const auto radial = isRadialAnimation();
668 
669 	const auto nameleft = _st.songPadding.left()
670 		+ _st.songThumbSize
671 		+ _st.songPadding.right();
672 	const auto nameright = _st.songPadding.left();
673 	const auto nametop = _st.songNameTop;
674 	const auto statustop = _st.songStatusTop;
675 	const auto namewidth = _width - nameleft - nameright;
676 
677 	const auto inner = style::rtlrect(
678 		_st.songPadding.left(),
679 		_st.songPadding.top(),
680 		_st.songThumbSize,
681 		_st.songThumbSize,
682 		_width);
683 	if (clip.intersects(inner)) {
684 		if (_data->hasThumbnail()) {
685 			ensureDataMediaCreated();
686 		}
687 		const auto thumbnail = _dataMedia
688 			? _dataMedia->thumbnail()
689 			: nullptr;
690 		const auto blurred = _dataMedia
691 			? _dataMedia->thumbnailInline()
692 			: nullptr;
693 
694 		p.setPen(Qt::NoPen);
695 		if (thumbnail || blurred) {
696 			const auto thumb = thumbnail
697 				? thumbnail->pixCircled(inner.width(), inner.height())
698 				: blurred->pixBlurredCircled(inner.width(), inner.height());
699 			p.drawPixmap(inner.topLeft(), thumb);
700 		} else if (_data->hasThumbnail()) {
701 			PainterHighQualityEnabler hq(p);
702 			p.setBrush(st::imageBg);
703 			p.drawEllipse(inner);
704 		}
705 		const auto &checkLink = (_data->loading() || _data->uploading())
706 			? _cancell
707 			: (_dataMedia->canBePlayed() || loaded)
708 			? _openl
709 			: _savel;
710 		if (selected) {
711 			p.setBrush((thumbnail || blurred) ? st::msgDateImgBgSelected : st::msgFileInBgSelected);
712 		} else if (_data->hasThumbnail()) {
713 			auto over = ClickHandler::showAsActive(checkLink);
714 			p.setBrush(anim::brush(st::msgDateImgBg, st::msgDateImgBgOver, _a_iconOver.value(over ? 1. : 0.)));
715 		} else {
716 			auto over = ClickHandler::showAsActive(checkLink);
717 			p.setBrush(anim::brush(st::msgFileInBg, st::msgFileInBgOver, _a_iconOver.value(over ? 1. : 0.)));
718 		}
719 		{
720 			PainterHighQualityEnabler hq(p);
721 			p.drawEllipse(inner);
722 		}
723 
724 		if (radial) {
725 			QRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine)));
726 			auto &bg = selected ? st::historyFileInRadialFgSelected : st::historyFileInRadialFg;
727 			_radial->draw(p, rinner, st::msgFileRadialLine, bg);
728 		}
729 
730 		const auto icon = [&] {
731 			if (_data->loading() || _data->uploading()) {
732 				return &(selected ? _st.voiceCancelSelected : _st.voiceCancel);
733 			} else if (showPause) {
734 				return &(selected ? _st.voicePauseSelected : _st.voicePause);
735 			} else if (_dataMedia->canBePlayed()) {
736 				return &(selected ? _st.voicePlaySelected : _st.voicePlay);
737 			}
738 			return &(selected
739 				? _st.voiceDownloadSelected
740 				: _st.voiceDownload);
741 		}();
742 		icon->paintInCenter(p, inner);
743 	}
744 
745 	if (clip.intersects(style::rtlrect(nameleft, nametop, namewidth, st::semiboldFont->height, _width))) {
746 		p.setPen(st::historyFileNameInFg);
747 		_name.drawLeftElided(p, nameleft, nametop, namewidth, _width);
748 	}
749 
750 	if (clip.intersects(style::rtlrect(nameleft, statustop, namewidth, st::normalFont->height, _width))) {
751 		p.setFont(st::normalFont);
752 		p.setPen(selected ? st::mediaInFgSelected : st::mediaInFg);
753 		int32 unreadx = nameleft;
754 		if (_status.size() == Ui::FileStatusSizeLoaded || _status.size() == Ui::FileStatusSizeReady) {
755 			p.setTextPalette(selected ? st::mediaInPaletteSelected : st::mediaInPalette);
756 			_details.drawLeftElided(p, nameleft, statustop, namewidth, _width);
757 			p.restoreTextPalette();
758 			unreadx += _details.maxWidth();
759 		} else {
760 			int32 statusw = st::normalFont->width(_status.text());
761 			p.drawTextLeft(nameleft, statustop, _width, _status.text(), statusw);
762 			unreadx += statusw;
763 		}
764 		if (parent()->hasUnreadMediaFlag() && unreadx + st::mediaUnreadSkip + st::mediaUnreadSize <= _width) {
765 			p.setPen(Qt::NoPen);
766 			p.setBrush(selected ? st::msgFileInBgSelected : st::msgFileInBg);
767 
768 			{
769 				PainterHighQualityEnabler hq(p);
770 				p.drawEllipse(style::rtlrect(unreadx + st::mediaUnreadSkip, statustop + st::mediaUnreadTop, st::mediaUnreadSize, st::mediaUnreadSize, _width));
771 			}
772 		}
773 	}
774 
775 	const auto checkDelta = _st.songThumbSize
776 		+ st::overviewCheckSkip
777 		- st::overviewSmallCheck.size;
778 	const auto checkLeft = _st.songPadding.left() + checkDelta;
779 	const auto checkTop = _st.songPadding.top() + checkDelta;
780 	paintCheckbox(p, { checkLeft, checkTop }, selected, context);
781 }
782 
getState(QPoint point,StateRequest request) const783 TextState Voice::getState(
784 		QPoint point,
785 		StateRequest request) const {
786 	ensureDataMediaCreated();
787 	const auto loaded = dataLoaded();
788 
789 	const auto nameleft = _st.songPadding.left()
790 		+ _st.songThumbSize
791 		+ _st.songPadding.right();
792 	const auto nameright = _st.songPadding.left();
793 	const auto nametop = _st.songNameTop;
794 	const auto statustop = _st.songStatusTop;
795 
796 	const auto inner = style::rtlrect(
797 		_st.songPadding.left(),
798 		_st.songPadding.top(),
799 		_st.songThumbSize,
800 		_st.songThumbSize,
801 		_width);
802 	if (inner.contains(point)) {
803 		const auto link = (_data->loading() || _data->uploading())
804 			? _cancell
805 			: (_dataMedia->canBePlayed() || loaded)
806 			? _openl
807 			: _savel;
808 		return { parent(), link };
809 	}
810 	auto result = TextState(parent());
811 	const auto statusmaxwidth = _width - nameleft - nameright;
812 	const auto statusrect = style::rtlrect(
813 		nameleft,
814 		statustop,
815 		statusmaxwidth,
816 		st::normalFont->height,
817 		_width);
818 	if (statusrect.contains(point)) {
819 		if (_status.size() == Ui::FileStatusSizeLoaded || _status.size() == Ui::FileStatusSizeReady) {
820 			auto textState = _details.getStateLeft(point - QPoint(nameleft, statustop), _width, _width);
821 			result.link = textState.link;
822 			result.cursor = textState.uponSymbol
823 				? HistoryView::CursorState::Text
824 				: HistoryView::CursorState::None;
825 		}
826 	}
827 	const auto namewidth = std::min(
828 		_width - nameleft - nameright,
829 		_name.maxWidth());
830 	const auto namerect = style::rtlrect(
831 		nameleft,
832 		nametop,
833 		namewidth,
834 		st::normalFont->height,
835 		_width);
836 	if (namerect.contains(point) && !result.link && !_data->loading()) {
837 		return { parent(), _namel };
838 	}
839 	return result;
840 }
841 
ensureDataMediaCreated() const842 void Voice::ensureDataMediaCreated() const {
843 	if (_dataMedia) {
844 		return;
845 	}
846 	_dataMedia = _data->createMediaView();
847 	delegate()->registerHeavyItem(this);
848 }
849 
clearHeavyPart()850 void Voice::clearHeavyPart() {
851 	_dataMedia = nullptr;
852 }
853 
dataProgress() const854 float64 Voice::dataProgress() const {
855 	ensureDataMediaCreated();
856 	return _dataMedia->progress();
857 }
858 
dataFinished() const859 bool Voice::dataFinished() const {
860 	return !_data->loading();
861 }
862 
dataLoaded() const863 bool Voice::dataLoaded() const {
864 	ensureDataMediaCreated();
865 	return _dataMedia->loaded();
866 }
867 
iconAnimated() const868 bool Voice::iconAnimated() const {
869 	return true;
870 }
871 
checkboxStyle() const872 const style::RoundCheckbox &Voice::checkboxStyle() const {
873 	return st::overviewSmallCheck;
874 }
875 
updateName()876 void Voice::updateName() {
877 	auto version = 0;
878 	if (const auto forwarded = parent()->Get<HistoryMessageForwarded>()) {
879 		if (parent()->fromOriginal()->isChannel()) {
880 			_name.setText(st::semiboldTextStyle, tr::lng_forwarded_channel(tr::now, lt_channel, parent()->fromOriginal()->name), Ui::NameTextOptions());
881 		} else {
882 			_name.setText(st::semiboldTextStyle, tr::lng_forwarded(tr::now, lt_user, parent()->fromOriginal()->name), Ui::NameTextOptions());
883 		}
884 	} else {
885 		_name.setText(st::semiboldTextStyle, parent()->from()->name, Ui::NameTextOptions());
886 	}
887 	version = parent()->fromOriginal()->nameVersion;
888 	_nameVersion = version;
889 }
890 
duration() const891 int Voice::duration() const {
892 	return std::max(_data->getDuration(), 0);
893 }
894 
updateStatusText()895 bool Voice::updateStatusText() {
896 	bool showPause = false;
897 	int32 statusSize = 0, realDuration = 0;
898 	if (_data->status == FileDownloadFailed || _data->status == FileUploadFailed) {
899 		statusSize = Ui::FileStatusSizeFailed;
900 	} else if (dataLoaded()) {
901 		statusSize = Ui::FileStatusSizeLoaded;
902 	} else {
903 		statusSize = Ui::FileStatusSizeReady;
904 	}
905 
906 	const auto state = Media::Player::instance()->getState(AudioMsgId::Type::Voice);
907 	if (state.id == AudioMsgId(_data, parent()->fullId(), state.id.externalPlayId())
908 		&& !Media::Player::IsStoppedOrStopping(state.state)) {
909 		statusSize = -1 - (state.position / state.frequency);
910 		realDuration = (state.length / state.frequency);
911 		showPause = Media::Player::ShowPauseIcon(state.state);
912 	}
913 
914 	if (statusSize != _status.size()) {
915 		_status.update(statusSize, _data->size, duration(), realDuration);
916 	}
917 	return showPause;
918 }
919 
Document(not_null<Delegate * > delegate,not_null<HistoryItem * > parent,not_null<DocumentData * > document,const style::OverviewFileLayout & st)920 Document::Document(
921 	not_null<Delegate*> delegate,
922 	not_null<HistoryItem*> parent,
923 	not_null<DocumentData*> document,
924 	const style::OverviewFileLayout &st)
925 : RadialProgressItem(delegate, parent)
926 , _data(document)
927 , _msgl(goToMessageClickHandler(parent))
928 , _namel(std::make_shared<DocumentOpenClickHandler>(
929 	_data,
930 	crl::guard(this, [=](FullMsgId id) {
931 		delegate->openDocument(_data, id);
932 	}),
933 	parent->fullId()))
934 , _st(st)
935 , _generic(::Layout::DocumentGenericPreview::Create(_data))
936 , _date(langDateTime(base::unixtime::parse(_data->date)))
937 , _ext(_generic.ext)
938 , _datew(st::normalFont->width(_date)) {
939 	_name.setMarkedText(
940 		st::defaultTextStyle,
941 		Ui::Text::FormatSongNameFor(_data).textWithEntities(),
942 		_documentNameOptions);
943 
944 	AddComponents(Info::Bit());
945 
946 	setDocumentLinks(_data);
947 
948 	_status.update(Ui::FileStatusSizeReady, _data->size, _data->isSong() ? _data->song()->duration : -1, 0);
949 
950 	if (withThumb()) {
951 		_data->loadThumbnail(parent->fullId());
952 		auto tw = style::ConvertScale(_data->thumbnailLocation().width());
953 		auto th = style::ConvertScale(_data->thumbnailLocation().height());
954 		if (tw > th) {
955 			_thumbw = (tw * _st.fileThumbSize) / th;
956 		} else {
957 			_thumbw = _st.fileThumbSize;
958 		}
959 	} else {
960 		_thumbw = 0;
961 	}
962 
963 	_extw = st::overviewFileExtFont->width(_ext);
964 	if (_extw > _st.fileThumbSize - st::overviewFileExtPadding * 2) {
965 		_ext = st::overviewFileExtFont->elided(_ext, _st.fileThumbSize - st::overviewFileExtPadding * 2, Qt::ElideMiddle);
966 		_extw = st::overviewFileExtFont->width(_ext);
967 	}
968 }
969 
downloadInCorner() const970 bool Document::downloadInCorner() const {
971 	return _data->isAudioFile()
972 		&& _data->canBeStreamed()
973 		&& !_data->inappPlaybackFailed()
974 		&& parent()->isRegular();
975 }
976 
initDimensions()977 void Document::initDimensions() {
978 	_maxw = _st.maxWidth;
979 	if (_data->isSong()) {
980 		_minh = _st.songPadding.top() + _st.songThumbSize + _st.songPadding.bottom();
981 	} else {
982 		_minh = _st.filePadding.top() + _st.fileThumbSize + _st.filePadding.bottom() + st::lineWidth;
983 	}
984 }
985 
paint(Painter & p,const QRect & clip,TextSelection selection,const PaintContext * context)986 void Document::paint(Painter &p, const QRect &clip, TextSelection selection, const PaintContext *context) {
987 	ensureDataMediaCreated();
988 
989 	const auto selected = (selection == FullSelection);
990 
991 	const auto cornerDownload = downloadInCorner();
992 
993 	_dataMedia->automaticLoad(parent()->fullId(), parent());
994 	const auto loaded = dataLoaded();
995 	const auto displayLoading = _data->displayLoading();
996 
997 	if (displayLoading) {
998 		ensureRadial();
999 		if (!_radial->animating()) {
1000 			_radial->start(dataProgress());
1001 		}
1002 	}
1003 	const auto showPause = updateStatusText();
1004 	const auto radial = isRadialAnimation();
1005 
1006 	int32 nameleft = 0, nametop = 0, nameright = 0, statustop = 0, datetop = -1;
1007 	const auto wthumb = withThumb();
1008 
1009 	const auto isSong = _data->isSong();
1010 	if (isSong) {
1011 		nameleft = _st.songPadding.left() + _st.songThumbSize + _st.songPadding.right();
1012 		nameright = _st.songPadding.left();
1013 		nametop = _st.songNameTop;
1014 		statustop = _st.songStatusTop;
1015 
1016 		auto inner = style::rtlrect(_st.songPadding.left(), _st.songPadding.top(), _st.songThumbSize, _st.songThumbSize, _width);
1017 		if (clip.intersects(inner)) {
1018 			const auto isLoading = (!cornerDownload
1019 				&& (_data->loading() || _data->uploading()));
1020 			p.setPen(Qt::NoPen);
1021 
1022 			using namespace HistoryView;
1023 			const auto coverDrawn = _data->isSongWithCover()
1024 				&& DrawThumbnailAsSongCover(
1025 					p,
1026 					st::songCoverOverlayFg,
1027 					_dataMedia,
1028 					inner,
1029 					selected);
1030 			if (!coverDrawn) {
1031 				if (selected) {
1032 					p.setBrush(st::msgFileInBgSelected);
1033 				} else {
1034 					const auto over = ClickHandler::showAsActive(isLoading
1035 						? _cancell
1036 						: (loaded || _dataMedia->canBePlayed())
1037 						? _openl
1038 						: _savel);
1039 					p.setBrush(anim::brush(
1040 						_st.songIconBg,
1041 						_st.songOverBg,
1042 						_a_iconOver.value(over ? 1. : 0.)));
1043 				}
1044 				PainterHighQualityEnabler hq(p);
1045 				p.drawEllipse(inner);
1046 			}
1047 
1048 			const auto icon = [&] {
1049 				if (!coverDrawn) {
1050 					if (isLoading) {
1051 						return &(selected
1052 							? _st.voiceCancelSelected
1053 							: _st.voiceCancel);
1054 					} else if (showPause) {
1055 						return &(selected
1056 							? _st.voicePauseSelected
1057 							: _st.voicePause);
1058 					} else if (loaded || _dataMedia->canBePlayed()) {
1059 						return &(selected
1060 							? _st.voicePlaySelected
1061 							: _st.voicePlay);
1062 					}
1063 					return &(selected
1064 						? _st.voiceDownloadSelected
1065 						: _st.voiceDownload);
1066 				}
1067 				if (isLoading) {
1068 					return &(selected ? _st.songCancelSelected : _st.songCancel);
1069 				} else if (showPause) {
1070 					return &(selected ? _st.songPauseSelected : _st.songPause);
1071 				} else if (loaded || _dataMedia->canBePlayed()) {
1072 					return &(selected ? _st.songPlaySelected : _st.songPlay);
1073 				}
1074 				return &(selected ? _st.songDownloadSelected : _st.songDownload);
1075 			}();
1076 			icon->paintInCenter(p, inner);
1077 
1078 			if (radial && !cornerDownload) {
1079 				auto rinner = inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine));
1080 				auto &bg = selected ? st::historyFileInRadialFgSelected : st::historyFileInRadialFg;
1081 				_radial->draw(p, rinner, st::msgFileRadialLine, bg);
1082 			}
1083 
1084 			drawCornerDownload(p, selected, context);
1085 		}
1086 	} else {
1087 		nameleft = _st.fileThumbSize + _st.filePadding.right();
1088 		nametop = st::linksBorder + _st.fileNameTop;
1089 		statustop = st::linksBorder + _st.fileStatusTop;
1090 		datetop = st::linksBorder + _st.fileDateTop;
1091 
1092 		QRect border(style::rtlrect(nameleft, 0, _width - nameleft, st::linksBorder, _width));
1093 		if (!context->isAfterDate && clip.intersects(border)) {
1094 			p.fillRect(clip.intersected(border), st::linksBorderFg);
1095 		}
1096 
1097 		QRect rthumb(style::rtlrect(0, st::linksBorder + _st.filePadding.top(), _st.fileThumbSize, _st.fileThumbSize, _width));
1098 		if (clip.intersects(rthumb)) {
1099 			if (wthumb) {
1100 				ensureDataMediaCreated();
1101 				const auto thumbnail = _dataMedia->thumbnail();
1102 				const auto blurred = _dataMedia->thumbnailInline();
1103 				if (thumbnail || blurred) {
1104 					if (_thumb.isNull() || (thumbnail && !_thumbLoaded)) {
1105 						_thumbLoaded = (thumbnail != nullptr);
1106 						auto options = Images::Option::Smooth
1107 							| (_thumbLoaded
1108 								? Images::Option::None
1109 								: Images::Option::Blurred);
1110 						const auto image = thumbnail ? thumbnail : blurred;
1111 						_thumb = image->pixNoCache(_thumbw * cIntRetinaFactor(), 0, options, _st.fileThumbSize, _st.fileThumbSize);
1112 					}
1113 					p.drawPixmap(rthumb.topLeft(), _thumb);
1114 				} else {
1115 					p.fillRect(rthumb, st::overviewFileThumbBg);
1116 				}
1117 			} else {
1118 				p.fillRect(rthumb, _generic.color);
1119 				if (!radial && loaded && !_ext.isEmpty()) {
1120 					p.setFont(st::overviewFileExtFont);
1121 					p.setPen(st::overviewFileExtFg);
1122 					p.drawText(rthumb.left() + (rthumb.width() - _extw) / 2, rthumb.top() + st::overviewFileExtTop + st::overviewFileExtFont->ascent, _ext);
1123 				}
1124 			}
1125 			if (selected) {
1126 				p.fillRect(rthumb, st::defaultTextPalette.selectOverlay);
1127 			}
1128 
1129 			if (radial || (!loaded && !_data->loading())) {
1130 				QRect inner(rthumb.x() + (rthumb.width() - _st.songThumbSize) / 2, rthumb.y() + (rthumb.height() - _st.songThumbSize) / 2, _st.songThumbSize, _st.songThumbSize);
1131 				if (clip.intersects(inner)) {
1132 					auto radialOpacity = (radial && loaded && !_data->uploading()) ? _radial->opacity() : 1;
1133 					p.setPen(Qt::NoPen);
1134 					if (selected) {
1135 						p.setBrush(wthumb
1136 							? st::msgDateImgBgSelected
1137 							: _generic.selected);
1138 					} else {
1139 						auto over = ClickHandler::showAsActive(_data->loading() ? _cancell : _savel);
1140 						p.setBrush(anim::brush(
1141 							wthumb ? st::msgDateImgBg : _generic.dark,
1142 							wthumb ? st::msgDateImgBgOver : _generic.over,
1143 							_a_iconOver.value(over ? 1. : 0.)));
1144 					}
1145 					p.setOpacity(radialOpacity * p.opacity());
1146 
1147 					{
1148 						PainterHighQualityEnabler hq(p);
1149 						p.drawEllipse(inner);
1150 					}
1151 
1152 					p.setOpacity(radialOpacity);
1153 					auto icon = ([loaded, this, selected] {
1154 						if (loaded || _data->loading()) {
1155 							return &(selected ? st::historyFileThumbCancelSelected : st::historyFileThumbCancel);
1156 						}
1157 						return &(selected ? st::historyFileThumbDownloadSelected : st::historyFileThumbDownload);
1158 					})();
1159 					icon->paintInCenter(p, inner);
1160 					if (radial) {
1161 						p.setOpacity(1);
1162 
1163 						QRect rinner(inner.marginsRemoved(QMargins(st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine, st::msgFileRadialLine)));
1164 						_radial->draw(p, rinner, st::msgFileRadialLine, selected ? st::historyFileThumbRadialFgSelected : st::historyFileThumbRadialFg);
1165 					}
1166 				}
1167 			}
1168 		}
1169 	}
1170 
1171 	const auto availwidth = _width - nameleft - nameright;
1172 	const auto namewidth = std::min(availwidth, _name.maxWidth());
1173 	if (clip.intersects(style::rtlrect(nameleft, nametop, namewidth, st::semiboldFont->height, _width))) {
1174 		p.setPen(st::historyFileNameInFg);
1175 		_name.drawLeftElided(p, nameleft, nametop, namewidth, _width);
1176 	}
1177 
1178 	if (clip.intersects(style::rtlrect(nameleft, statustop, availwidth, st::normalFont->height, _width))) {
1179 		p.setFont(st::normalFont);
1180 		p.setPen((isSong && selected) ? st::mediaInFgSelected : st::mediaInFg);
1181 		p.drawTextLeft(nameleft, statustop, _width, _status.text());
1182 	}
1183 	if (datetop >= 0 && clip.intersects(style::rtlrect(nameleft, datetop, _datew, st::normalFont->height, _width))) {
1184 		p.setFont(ClickHandler::showAsActive(_msgl) ? st::normalFont->underline() : st::normalFont);
1185 		p.setPen(st::mediaInFg);
1186 		p.drawTextLeft(nameleft, datetop, _width, _date, _datew);
1187 	}
1188 
1189 	const auto checkDelta = (isSong ? _st.songThumbSize : _st.fileThumbSize)
1190 		+ (isSong ? st::overviewCheckSkip : -st::overviewCheckSkip)
1191 		- st::overviewSmallCheck.size;
1192 	const auto checkLeft = (isSong
1193 		? _st.songPadding.left()
1194 		: 0) + checkDelta;
1195 	const auto checkTop = (isSong
1196 		? _st.songPadding.top()
1197 		: (st::linksBorder + _st.filePadding.top())) + checkDelta;
1198 	paintCheckbox(p, { checkLeft, checkTop }, selected, context);
1199 }
1200 
drawCornerDownload(Painter & p,bool selected,const PaintContext * context) const1201 void Document::drawCornerDownload(Painter &p, bool selected, const PaintContext *context) const {
1202 	if (dataLoaded()
1203 		|| _data->loadedInMediaCache()
1204 		|| !downloadInCorner()) {
1205 		return;
1206 	}
1207 	const auto size = st::overviewSmallCheck.size;
1208 	const auto shift = _st.songThumbSize + st::overviewCheckSkip - size;
1209 	const auto inner = style::rtlrect(_st.songPadding.left() + shift, _st.songPadding.top() + shift, size, size, _width);
1210 	auto pen = st::windowBg->p;
1211 	pen.setWidth(st::lineWidth);
1212 	p.setPen(pen);
1213 	if (selected) {
1214 		p.setBrush(st::msgFileInBgSelected);
1215 	} else {
1216 		p.setBrush(_st.songIconBg);
1217 	}
1218 	{
1219 		PainterHighQualityEnabler hq(p);
1220 		p.drawEllipse(inner);
1221 	}
1222 	const auto icon = [&] {
1223 		if (_data->loading()) {
1224 			return &(selected ? st::overviewSmallCancelSelected : st::overviewSmallCancel);
1225 		}
1226 		return &(selected ? st::overviewSmallDownloadSelected : st::overviewSmallDownload);
1227 	}();
1228 	icon->paintInCenter(p, inner);
1229 	if (_radial && _radial->animating()) {
1230 		const auto rinner = inner.marginsRemoved(QMargins(st::historyAudioRadialLine, st::historyAudioRadialLine, st::historyAudioRadialLine, st::historyAudioRadialLine));
1231 		auto fg = selected ? st::historyFileThumbRadialFgSelected : st::historyFileThumbRadialFg;
1232 		_radial->draw(p, rinner, st::historyAudioRadialLine, fg);
1233 	}
1234 }
1235 
cornerDownloadTextState(QPoint point,StateRequest request) const1236 TextState Document::cornerDownloadTextState(
1237 		QPoint point,
1238 		StateRequest request) const {
1239 	auto result = TextState(parent());
1240 	if (!downloadInCorner()
1241 		|| dataLoaded()
1242 		|| _data->loadedInMediaCache()) {
1243 		return result;
1244 	}
1245 	const auto size = st::overviewSmallCheck.size;
1246 	const auto shift = _st.songThumbSize + st::overviewCheckSkip - size;
1247 	const auto inner = style::rtlrect(_st.songPadding.left() + shift, _st.songPadding.top() + shift, size, size, _width);
1248 	if (inner.contains(point)) {
1249 		result.link = _data->loading() ? _cancell : _savel;
1250 	}
1251 	return result;
1252 
1253 }
1254 
getState(QPoint point,StateRequest request) const1255 TextState Document::getState(
1256 		QPoint point,
1257 		StateRequest request) const {
1258 	ensureDataMediaCreated();
1259 	const auto loaded = dataLoaded();
1260 
1261 	if (_data->isSong()) {
1262 		const auto nameleft = _st.songPadding.left() + _st.songThumbSize + _st.songPadding.right();
1263 		const auto nameright = _st.songPadding.left();
1264 		const auto namewidth = std::min(
1265 			_width - nameleft - nameright,
1266 			_name.maxWidth());
1267 		const auto nametop = _st.songNameTop;
1268 
1269 		if (const auto state = cornerDownloadTextState(point, request); state.link) {
1270 			return state;
1271 		}
1272 
1273 		const auto inner = style::rtlrect(
1274 			_st.songPadding.left(),
1275 			_st.songPadding.top(),
1276 			_st.songThumbSize,
1277 			_st.songThumbSize,
1278 			_width);
1279 		if (inner.contains(point)) {
1280 			const auto link = (!downloadInCorner()
1281 				&& (_data->loading() || _data->uploading()))
1282 				? _cancell
1283 				: (loaded || _dataMedia->canBePlayed())
1284 				? _openl
1285 				: _savel;
1286 			return { parent(), link };
1287 		}
1288 		const auto namerect = style::rtlrect(
1289 			nameleft,
1290 			nametop,
1291 			namewidth,
1292 			st::semiboldFont->height,
1293 			_width);
1294 		if (namerect.contains(point) && !_data->loading()) {
1295 			return { parent(), _namel };
1296 		}
1297 	} else {
1298 		const auto nameleft = _st.fileThumbSize + _st.filePadding.right();
1299 		const auto nameright = 0;
1300 		const auto nametop = st::linksBorder + _st.fileNameTop;
1301 		const auto namewidth = std::min(
1302 			_width - nameleft - nameright,
1303 			_name.maxWidth());
1304 		const auto datetop = st::linksBorder + _st.fileDateTop;
1305 
1306 		const auto rthumb = style::rtlrect(
1307 			0,
1308 			st::linksBorder + _st.filePadding.top(),
1309 			_st.fileThumbSize,
1310 			_st.fileThumbSize,
1311 			_width);
1312 
1313 		if (rthumb.contains(point)) {
1314 			const auto link = (_data->loading() || _data->uploading())
1315 				? _cancell
1316 				: loaded
1317 				? _openl
1318 				: _savel;
1319 			return { parent(), link };
1320 		}
1321 
1322 		if (_data->status != FileUploadFailed) {
1323 			auto daterect = style::rtlrect(
1324 				nameleft,
1325 				datetop,
1326 				_datew,
1327 				st::normalFont->height,
1328 				_width);
1329 			if (daterect.contains(point)) {
1330 				return { parent(), _msgl };
1331 			}
1332 		}
1333 		if (!_data->loading() && !_data->isNull()) {
1334 			auto leftofnamerect = style::rtlrect(
1335 				0,
1336 				st::linksBorder,
1337 				nameleft,
1338 				_height - st::linksBorder,
1339 				_width);
1340 			if (loaded && leftofnamerect.contains(point)) {
1341 				return { parent(), _namel };
1342 			}
1343 			const auto namerect = style::rtlrect(
1344 				nameleft,
1345 				nametop,
1346 				namewidth,
1347 				st::semiboldFont->height,
1348 				_width);
1349 			if (namerect.contains(point)) {
1350 				return { parent(), _namel };
1351 			}
1352 		}
1353 	}
1354 	return {};
1355 }
1356 
checkboxStyle() const1357 const style::RoundCheckbox &Document::checkboxStyle() const {
1358 	return st::overviewSmallCheck;
1359 }
1360 
ensureDataMediaCreated() const1361 void Document::ensureDataMediaCreated() const {
1362 	if (_dataMedia) {
1363 		return;
1364 	}
1365 	_dataMedia = _data->createMediaView();
1366 	_dataMedia->thumbnailWanted(parent()->fullId());
1367 	delegate()->registerHeavyItem(this);
1368 }
1369 
clearHeavyPart()1370 void Document::clearHeavyPart() {
1371 	_dataMedia = nullptr;
1372 }
1373 
dataProgress() const1374 float64 Document::dataProgress() const {
1375 	ensureDataMediaCreated();
1376 	return _dataMedia->progress();
1377 }
1378 
dataFinished() const1379 bool Document::dataFinished() const {
1380 	return !_data->loading();
1381 }
1382 
dataLoaded() const1383 bool Document::dataLoaded() const {
1384 	ensureDataMediaCreated();
1385 	return _dataMedia->loaded();
1386 }
1387 
iconAnimated() const1388 bool Document::iconAnimated() const {
1389 	return _data->isSong()
1390 		|| !dataLoaded()
1391 		|| (_radial && _radial->animating());
1392 }
1393 
withThumb() const1394 bool Document::withThumb() const {
1395 	return !_data->isSong()
1396 		&& _data->hasThumbnail()
1397 		&& !Data::IsExecutableName(_data->filename());
1398 }
1399 
updateStatusText()1400 bool Document::updateStatusText() {
1401 	bool showPause = false;
1402 	int32 statusSize = 0, realDuration = 0;
1403 	if (_data->status == FileDownloadFailed
1404 		|| _data->status == FileUploadFailed) {
1405 		statusSize = Ui::FileStatusSizeFailed;
1406 	} else if (_data->uploading()) {
1407 		statusSize = _data->uploadingData->offset;
1408 	} else if (_data->loading()) {
1409 		statusSize = _data->loadOffset();
1410 	} else if (dataLoaded()) {
1411 		statusSize = Ui::FileStatusSizeLoaded;
1412 	} else {
1413 		statusSize = Ui::FileStatusSizeReady;
1414 	}
1415 
1416 	if (_data->isSong()) {
1417 		const auto state = Media::Player::instance()->getState(AudioMsgId::Type::Song);
1418 		if (state.id == AudioMsgId(_data, parent()->fullId(), state.id.externalPlayId()) && !Media::Player::IsStoppedOrStopping(state.state)) {
1419 			statusSize = -1 - (state.position / state.frequency);
1420 			realDuration = (state.length / state.frequency);
1421 			showPause = Media::Player::ShowPauseIcon(state.state);
1422 		}
1423 		if (!showPause && (state.id == AudioMsgId(_data, parent()->fullId(), state.id.externalPlayId())) && Media::Player::instance()->isSeeking(AudioMsgId::Type::Song)) {
1424 			showPause = true;
1425 		}
1426 	}
1427 
1428 	if (statusSize != _status.size()) {
1429 		_status.update(statusSize, _data->size, _data->isSong() ? _data->song()->duration : -1, realDuration);
1430 	}
1431 	return showPause;
1432 }
1433 
Link(not_null<Delegate * > delegate,not_null<HistoryItem * > parent,Data::Media * media)1434 Link::Link(
1435 	not_null<Delegate*> delegate,
1436 	not_null<HistoryItem*> parent,
1437 	Data::Media *media)
1438 : ItemBase(delegate, parent) {
1439 	AddComponents(Info::Bit());
1440 
1441 	auto textWithEntities = parent->originalText();
1442 	QString mainUrl;
1443 
1444 	auto text = textWithEntities.text;
1445 	const auto &entities = textWithEntities.entities;
1446 	int32 from = 0, till = text.size(), lnk = entities.size();
1447 	for (const auto &entity : entities) {
1448 		auto type = entity.type();
1449 		if (type != EntityType::Url && type != EntityType::CustomUrl && type != EntityType::Email) {
1450 			continue;
1451 		}
1452 		const auto customUrl = entity.data();
1453 		const auto entityText = text.mid(entity.offset(), entity.length());
1454 		const auto url = customUrl.isEmpty() ? entityText : customUrl;
1455 		if (_links.isEmpty()) {
1456 			mainUrl = url;
1457 		}
1458 		_links.push_back(LinkEntry(url, entityText));
1459 	}
1460 	while (lnk > 0 && till > from) {
1461 		--lnk;
1462 		auto &entity = entities.at(lnk);
1463 		auto type = entity.type();
1464 		if (type != EntityType::Url && type != EntityType::CustomUrl && type != EntityType::Email) {
1465 			++lnk;
1466 			break;
1467 		}
1468 		int32 afterLinkStart = entity.offset() + entity.length();
1469 		if (till > afterLinkStart) {
1470 			if (!QRegularExpression(qsl("^[,.\\s_=+\\-;:`'\"\\(\\)\\[\\]\\{\\}<>*&^%\\$#@!\\\\/]+$")).match(text.mid(afterLinkStart, till - afterLinkStart)).hasMatch()) {
1471 				++lnk;
1472 				break;
1473 			}
1474 		}
1475 		till = entity.offset();
1476 	}
1477 	if (!lnk) {
1478 		if (QRegularExpression(qsl("^[,.\\s\\-;:`'\"\\(\\)\\[\\]\\{\\}<>*&^%\\$#@!\\\\/]+$")).match(text.mid(from, till - from)).hasMatch()) {
1479 			till = from;
1480 		}
1481 	}
1482 
1483 	const auto createHandler = [](const QString &url) {
1484 		return UrlClickHandler::IsSuspicious(url)
1485 			? std::make_shared<HiddenUrlClickHandler>(url)
1486 			: std::make_shared<UrlClickHandler>(url);
1487 	};
1488 	_page = media ? media->webpage() : nullptr;
1489 	if (_page) {
1490 		mainUrl = _page->url;
1491 		if (_page->document) {
1492 			_photol = std::make_shared<DocumentOpenClickHandler>(
1493 				_page->document,
1494 				crl::guard(this, [=](FullMsgId id) {
1495 					delegate->openDocument(_page->document, id);
1496 				}),
1497 				parent->fullId());
1498 		} else if (_page->photo) {
1499 			if (_page->type == WebPageType::Profile || _page->type == WebPageType::Video) {
1500 				_photol = createHandler(_page->url);
1501 			} else if (_page->type == WebPageType::Photo
1502 				|| _page->siteName == qstr("Twitter")
1503 				|| _page->siteName == qstr("Facebook")) {
1504 				_photol = std::make_shared<PhotoOpenClickHandler>(
1505 					_page->photo,
1506 					crl::guard(this, [=](FullMsgId id) {
1507 						delegate->openPhoto(_page->photo, id);
1508 					}),
1509 					parent->fullId());
1510 			} else {
1511 				_photol = createHandler(_page->url);
1512 			}
1513 		} else {
1514 			_photol = createHandler(_page->url);
1515 		}
1516 	} else if (!mainUrl.isEmpty()) {
1517 		_photol = createHandler(mainUrl);
1518 	}
1519 	if (from >= till && _page) {
1520 		text = _page->description.text;
1521 		from = 0;
1522 		till = text.size();
1523 	}
1524 	if (till > from) {
1525 		TextParseOptions opts = { TextParseMultiline, int32(st::linksMaxWidth), 3 * st::normalFont->height, Qt::LayoutDirectionAuto };
1526 		_text.setText(st::defaultTextStyle, text.mid(from, till - from), opts);
1527 	}
1528 	int32 tw = 0, th = 0;
1529 	if (_page && _page->photo) {
1530 		const auto photo = _page->photo;
1531 		if (photo->hasExact(Data::PhotoSize::Small)
1532 			|| photo->hasExact(Data::PhotoSize::Thumbnail)) {
1533 			photo->load(Data::PhotoSize::Small, parent->fullId());
1534 		}
1535 		tw = style::ConvertScale(photo->width());
1536 		th = style::ConvertScale(photo->height());
1537 	} else if (_page && _page->document && _page->document->hasThumbnail()) {
1538 		_page->document->loadThumbnail(parent->fullId());
1539 		const auto &location = _page->document->thumbnailLocation();
1540 		tw = style::ConvertScale(location.width());
1541 		th = style::ConvertScale(location.height());
1542 	}
1543 	if (tw > st::linksPhotoSize) {
1544 		if (th > tw) {
1545 			th = th * st::linksPhotoSize / tw;
1546 			tw = st::linksPhotoSize;
1547 		} else if (th > st::linksPhotoSize) {
1548 			tw = tw * st::linksPhotoSize / th;
1549 			th = st::linksPhotoSize;
1550 		}
1551 	}
1552 	_pixw = qMax(tw, 1);
1553 	_pixh = qMax(th, 1);
1554 
1555 	if (_page) {
1556 		_title = _page->title;
1557 	}
1558 
1559 	auto parts = QStringView(mainUrl).split('/');
1560 	if (!parts.isEmpty()) {
1561 		auto domain = parts.at(0);
1562 		if (parts.size() > 2 && domain.endsWith(':') && parts.at(1).isEmpty()) { // http:// and others
1563 			domain = parts.at(2);
1564 		}
1565 
1566 		parts = domain.split('@').back().split('.', Qt::SkipEmptyParts);
1567 		if (parts.size() > 1) {
1568 			_letter = parts.at(parts.size() - 2).at(0).toUpper();
1569 			if (_title.isEmpty()) {
1570 				_title.reserve(parts.at(parts.size() - 2).size());
1571 				_title.append(_letter).append(parts.at(parts.size() - 2).mid(1));
1572 			}
1573 		}
1574 	}
1575 	_titlew = st::semiboldFont->width(_title);
1576 }
1577 
initDimensions()1578 void Link::initDimensions() {
1579 	_maxw = st::linksMaxWidth;
1580 	_minh = 0;
1581 	if (!_title.isEmpty()) {
1582 		_minh += st::semiboldFont->height;
1583 	}
1584 	if (!_text.isEmpty()) {
1585 		_minh += qMin(3 * st::normalFont->height, _text.countHeight(_maxw - st::linksPhotoSize - st::linksPhotoPadding));
1586 	}
1587 	_minh += _links.size() * st::normalFont->height;
1588 	_minh = qMax(_minh, int32(st::linksPhotoSize)) + st::linksMargin.top() + st::linksMargin.bottom() + st::linksBorder;
1589 }
1590 
resizeGetHeight(int32 width)1591 int32 Link::resizeGetHeight(int32 width) {
1592 	_width = qMin(width, _maxw);
1593 	int32 w = _width - st::linksPhotoSize - st::linksPhotoPadding;
1594 	for (const auto &link : std::as_const(_links)) {
1595 		link.lnk->setFullDisplayed(w >= link.width);
1596 	}
1597 
1598 	_height = 0;
1599 	if (!_title.isEmpty()) {
1600 		_height += st::semiboldFont->height;
1601 	}
1602 	if (!_text.isEmpty()) {
1603 		_height += qMin(3 * st::normalFont->height, _text.countHeight(_width - st::linksPhotoSize - st::linksPhotoPadding));
1604 	}
1605 	_height += _links.size() * st::normalFont->height;
1606 	_height = qMax(_height, int32(st::linksPhotoSize)) + st::linksMargin.top() + st::linksMargin.bottom() + st::linksBorder;
1607 	return _height;
1608 }
1609 
paint(Painter & p,const QRect & clip,TextSelection selection,const PaintContext * context)1610 void Link::paint(Painter &p, const QRect &clip, TextSelection selection, const PaintContext *context) {
1611 	auto selected = (selection == FullSelection);
1612 
1613 	const auto pixLeft = 0;
1614 	const auto pixTop = st::linksMargin.top() + st::linksBorder;
1615 	if (clip.intersects(style::rtlrect(0, pixTop, st::linksPhotoSize, st::linksPhotoSize, _width))) {
1616 		validateThumbnail();
1617 		if (!_thumbnail.isNull()) {
1618 			p.drawPixmap(pixLeft, pixTop, _thumbnail);
1619 		}
1620 	}
1621 
1622 	const auto left = st::linksPhotoSize + st::linksPhotoPadding;
1623 	const auto w = _width - left;
1624 	auto top = [&] {
1625 		if (!_title.isEmpty() && _text.isEmpty() && _links.size() == 1) {
1626 			return pixTop + (st::linksPhotoSize - st::semiboldFont->height - st::normalFont->height) / 2;
1627 		}
1628 		return st::linksTextTop;
1629 	}();
1630 
1631 	p.setPen(st::linksTextFg);
1632 	p.setFont(st::semiboldFont);
1633 	if (!_title.isEmpty()) {
1634 		if (clip.intersects(style::rtlrect(left, top, qMin(w, _titlew), st::semiboldFont->height, _width))) {
1635 			p.drawTextLeft(left, top, _width, (w < _titlew) ? st::semiboldFont->elided(_title, w) : _title);
1636 		}
1637 		top += st::semiboldFont->height;
1638 	}
1639 	p.setFont(st::msgFont);
1640 	if (!_text.isEmpty()) {
1641 		int32 h = qMin(st::normalFont->height * 3, _text.countHeight(w));
1642 		if (clip.intersects(style::rtlrect(left, top, w, h, _width))) {
1643 			_text.drawLeftElided(p, left, top, w, _width, 3);
1644 		}
1645 		top += h;
1646 	}
1647 
1648 	p.setPen(st::windowActiveTextFg);
1649 	for (const auto &link : std::as_const(_links)) {
1650 		if (clip.intersects(style::rtlrect(left, top, qMin(w, link.width), st::normalFont->height, _width))) {
1651 			p.setFont(ClickHandler::showAsActive(link.lnk) ? st::normalFont->underline() : st::normalFont);
1652 			p.drawTextLeft(left, top, _width, (w < link.width) ? st::normalFont->elided(link.text, w) : link.text);
1653 		}
1654 		top += st::normalFont->height;
1655 	}
1656 
1657 	QRect border(style::rtlrect(left, 0, w, st::linksBorder, _width));
1658 	if (!context->isAfterDate && clip.intersects(border)) {
1659 		p.fillRect(clip.intersected(border), st::linksBorderFg);
1660 	}
1661 
1662 	const auto checkDelta = st::linksPhotoSize + st::overviewCheckSkip
1663 		- st::overviewSmallCheck.size;
1664 	const auto checkLeft = pixLeft + checkDelta;
1665 	const auto checkTop = pixTop + checkDelta;
1666 	paintCheckbox(p, { checkLeft, checkTop }, selected, context);
1667 }
1668 
validateThumbnail()1669 void Link::validateThumbnail() {
1670 	if (!_thumbnail.isNull() && !_thumbnailBlurred) {
1671 		return;
1672 	}
1673 	if (_page && _page->photo) {
1674 		using Data::PhotoSize;
1675 		ensurePhotoMediaCreated();
1676 		if (const auto thumbnail = _photoMedia->image(PhotoSize::Thumbnail)) {
1677 			_thumbnail = thumbnail->pixSingle(_pixw, _pixh, st::linksPhotoSize, st::linksPhotoSize, ImageRoundRadius::Small);
1678 			_thumbnailBlurred = false;
1679 		} else if (const auto large = _photoMedia->image(PhotoSize::Large)) {
1680 			_thumbnail = large->pixSingle(_pixw, _pixh, st::linksPhotoSize, st::linksPhotoSize, ImageRoundRadius::Small);
1681 			_thumbnailBlurred = false;
1682 		} else if (const auto small = _photoMedia->image(PhotoSize::Small)) {
1683 			_thumbnail = small->pixSingle(_pixw, _pixh, st::linksPhotoSize, st::linksPhotoSize, ImageRoundRadius::Small);
1684 			_thumbnailBlurred = false;
1685 		} else if (const auto blurred = _photoMedia->thumbnailInline()) {
1686 			_thumbnail = blurred->pixBlurredSingle(_pixw, _pixh, st::linksPhotoSize, st::linksPhotoSize, ImageRoundRadius::Small);
1687 			return;
1688 		} else {
1689 			return;
1690 		}
1691 		_photoMedia = nullptr;
1692 		delegate()->unregisterHeavyItem(this);
1693 	} else if (_page && _page->document && _page->document->hasThumbnail()) {
1694 		ensureDocumentMediaCreated();
1695 		const auto roundRadius = _page->document->isVideoMessage()
1696 			? ImageRoundRadius::Ellipse
1697 			: ImageRoundRadius::Small;
1698 		if (const auto thumbnail = _documentMedia->thumbnail()) {
1699 			_thumbnail = thumbnail->pixSingle(_pixw, _pixh, st::linksPhotoSize, st::linksPhotoSize, roundRadius);
1700 			_thumbnailBlurred = false;
1701 		} else if (const auto blurred = _documentMedia->thumbnailInline()) {
1702 			_thumbnail = blurred->pixBlurredSingle(_pixw, _pixh, st::linksPhotoSize, st::linksPhotoSize, roundRadius);
1703 			return;
1704 		} else {
1705 			return;
1706 		}
1707 		_documentMedia = nullptr;
1708 		delegate()->unregisterHeavyItem(this);
1709 	} else {
1710 		const auto size = QSize(st::linksPhotoSize, st::linksPhotoSize);
1711 		_thumbnail = QPixmap(size * cIntRetinaFactor());
1712 		_thumbnail.fill(Qt::transparent);
1713 		auto p = Painter(&_thumbnail);
1714 		const auto index = _letter.isEmpty()
1715 			? 0
1716 			: (_letter[0].unicode() % 4);
1717 		const auto fill = [&](style::color color, Ui::CachedRoundCorners corners) {
1718 			auto pixRect = QRect(
1719 				0,
1720 				0,
1721 				st::linksPhotoSize,
1722 				st::linksPhotoSize);
1723 			Ui::FillRoundRect(p, pixRect, color, corners);
1724 		};
1725 		switch (index) {
1726 		case 0: fill(st::msgFile1Bg, Ui::Doc1Corners); break;
1727 		case 1: fill(st::msgFile2Bg, Ui::Doc2Corners); break;
1728 		case 2: fill(st::msgFile3Bg, Ui::Doc3Corners); break;
1729 		case 3: fill(st::msgFile4Bg, Ui::Doc4Corners); break;
1730 		}
1731 
1732 		if (!_letter.isEmpty()) {
1733 			p.setFont(st::linksLetterFont);
1734 			p.setPen(st::linksLetterFg);
1735 			p.drawText(
1736 				QRect(0, 0, st::linksPhotoSize, st::linksPhotoSize),
1737 				_letter,
1738 				style::al_center);
1739 		}
1740 		_thumbnailBlurred = false;
1741 	}
1742 }
1743 
ensurePhotoMediaCreated()1744 void Link::ensurePhotoMediaCreated() {
1745 	if (_photoMedia) {
1746 		return;
1747 	}
1748 	_photoMedia = _page->photo->createMediaView();
1749 	_photoMedia->wanted(Data::PhotoSize::Small, parent()->fullId());
1750 	delegate()->registerHeavyItem(this);
1751 }
1752 
ensureDocumentMediaCreated()1753 void Link::ensureDocumentMediaCreated() {
1754 	if (_documentMedia) {
1755 		return;
1756 	}
1757 	_documentMedia = _page->document->createMediaView();
1758 	_documentMedia->thumbnailWanted(parent()->fullId());
1759 	delegate()->registerHeavyItem(this);
1760 }
1761 
clearHeavyPart()1762 void Link::clearHeavyPart() {
1763 	_photoMedia = nullptr;
1764 	_documentMedia = nullptr;
1765 }
1766 
getState(QPoint point,StateRequest request) const1767 TextState Link::getState(
1768 		QPoint point,
1769 		StateRequest request) const {
1770 	int32 left = st::linksPhotoSize + st::linksPhotoPadding, top = st::linksMargin.top() + st::linksBorder, w = _width - left;
1771 	if (style::rtlrect(0, top, st::linksPhotoSize, st::linksPhotoSize, _width).contains(point)) {
1772 		return { parent(), _photol };
1773 	}
1774 
1775 	if (!_title.isEmpty() && _text.isEmpty() && _links.size() == 1) {
1776 		top += (st::linksPhotoSize - st::semiboldFont->height - st::normalFont->height) / 2;
1777 	}
1778 	if (!_title.isEmpty()) {
1779 		if (style::rtlrect(left, top, qMin(w, _titlew), st::semiboldFont->height, _width).contains(point)) {
1780 			return { parent(), _photol };
1781 		}
1782 		top += st::webPageTitleFont->height;
1783 	}
1784 	if (!_text.isEmpty()) {
1785 		top += qMin(st::normalFont->height * 3, _text.countHeight(w));
1786 	}
1787 	for (const auto &link : _links) {
1788 		if (style::rtlrect(left, top, qMin(w, link.width), st::normalFont->height, _width).contains(point)) {
1789 			return { parent(), ClickHandlerPtr(link.lnk) };
1790 		}
1791 		top += st::normalFont->height;
1792 	}
1793 	return {};
1794 }
1795 
checkboxStyle() const1796 const style::RoundCheckbox &Link::checkboxStyle() const {
1797 	return st::overviewSmallCheck;
1798 }
1799 
LinkEntry(const QString & url,const QString & text)1800 Link::LinkEntry::LinkEntry(const QString &url, const QString &text)
1801 : text(text)
1802 , width(st::normalFont->width(text))
1803 , lnk(UrlClickHandler::IsSuspicious(url)
1804 	? std::make_shared<HiddenUrlClickHandler>(url)
1805 	: std::make_shared<UrlClickHandler>(url)) {
1806 }
1807 
1808 // Copied from inline_bot_layout_internal.
Gif(not_null<Delegate * > delegate,not_null<HistoryItem * > parent,not_null<DocumentData * > gif)1809 Gif::Gif(
1810 	not_null<Delegate*> delegate,
1811 	not_null<HistoryItem*> parent,
1812 	not_null<DocumentData*> gif)
1813 : RadialProgressItem(delegate, parent)
1814 , _data(gif) {
1815 	setDocumentLinks(_data, true);
1816 	_data->loadThumbnail(parent->fullId());
1817 }
1818 
1819 Gif::~Gif() = default;
1820 
contentWidth() const1821 int Gif::contentWidth() const {
1822 	if (_data->dimensions.width() > 0) {
1823 		return _data->dimensions.width();
1824 	}
1825 	return style::ConvertScale(_data->thumbnailLocation().width());
1826 }
1827 
contentHeight() const1828 int Gif::contentHeight() const {
1829 	if (_data->dimensions.height() > 0) {
1830 		return _data->dimensions.height();
1831 	}
1832 	return style::ConvertScale(_data->thumbnailLocation().height());
1833 }
1834 
initDimensions()1835 void Gif::initDimensions() {
1836 	int32 w = contentWidth(), h = contentHeight();
1837 	if (w <= 0 || h <= 0) {
1838 		_maxw = 0;
1839 	} else {
1840 		w = w * st::inlineMediaHeight / h;
1841 		_maxw = qMax(w, int32(st::inlineResultsMinWidth));
1842 	}
1843 	_minh = st::inlineMediaHeight + st::inlineResultsSkip;
1844 }
1845 
resizeGetHeight(int32 width)1846 int32 Gif::resizeGetHeight(int32 width) {
1847 	_width = width;
1848 	_height = _minh;
1849 	return _height;
1850 }
1851 
countFrameSize() const1852 QSize Gif::countFrameSize() const {
1853 	const auto animating = (_gif && _gif->ready());
1854 	auto framew = animating ? _gif->width() : contentWidth();
1855 	auto frameh = animating ? _gif->height() : contentHeight();
1856 	const auto height = st::inlineMediaHeight;
1857 	const auto maxSize = st::maxStickerSize;
1858 	if (framew * height > frameh * _width) {
1859 		if (framew < maxSize || frameh > height) {
1860 			if (frameh > height || (framew * height / frameh) <= maxSize) {
1861 				framew = framew * height / frameh;
1862 				frameh = height;
1863 			} else {
1864 				frameh = int32(frameh * maxSize) / framew;
1865 				framew = maxSize;
1866 			}
1867 		}
1868 	} else {
1869 		if (frameh < maxSize || framew > _width) {
1870 			if (framew > _width || (frameh * _width / framew) <= maxSize) {
1871 				frameh = frameh * _width / framew;
1872 				framew = _width;
1873 			} else {
1874 				framew = int32(framew * maxSize) / frameh;
1875 				frameh = maxSize;
1876 			}
1877 		}
1878 	}
1879 	return QSize(framew, frameh);
1880 }
1881 
clipCallback(Media::Clip::Notification notification)1882 void Gif::clipCallback(Media::Clip::Notification notification) {
1883 	using namespace Media::Clip;
1884 	switch (notification) {
1885 	case NotificationReinit: {
1886 		if (_gif) {
1887 			if (_gif->state() == State::Error) {
1888 				_gif.setBad();
1889 			} else if (_gif->ready() && !_gif->started()) {
1890 				if (_gif->width() * _gif->height() > kMaxInlineArea) {
1891 					_data->dimensions = QSize(
1892 						_gif->width(),
1893 						_gif->height());
1894 					_gif.reset();
1895 				} else {
1896 					auto height = st::inlineMediaHeight;
1897 					auto frame = countFrameSize();
1898 					_gif->start(
1899 						frame.width(),
1900 						frame.height(),
1901 						_width,
1902 						height,
1903 						ImageRoundRadius::None, RectPart::None);
1904 				}
1905 			} else if (_gif->autoPausedGif()
1906 					&& !delegate()->itemVisible(this)) {
1907 				clearHeavyPart();
1908 			}
1909 		}
1910 
1911 		update();
1912 	} break;
1913 
1914 	case NotificationRepaint: {
1915 		if (_gif && !_gif->currentDisplayed()) {
1916 			update();
1917 		}
1918 	} break;
1919 	}
1920 }
1921 
validateThumbnail(Image * image,QSize size,QSize frame,bool good)1922 void Gif::validateThumbnail(
1923 		Image *image,
1924 		QSize size,
1925 		QSize frame,
1926 		bool good) {
1927 	if (!image || (_thumbGood && !good)) {
1928 		return;
1929 	} else if ((_thumb.size() == size * cIntRetinaFactor())
1930 		&& (_thumbGood || !good)) {
1931 		return;
1932 	}
1933 	_thumbGood = good;
1934 	_thumb = image->pixNoCache(
1935 		frame.width() * cIntRetinaFactor(),
1936 		frame.height() * cIntRetinaFactor(),
1937 		(Images::Option::Smooth
1938 			| (good ? Images::Option::None : Images::Option::Blurred)),
1939 		size.width(),
1940 		size.height());
1941 }
1942 
prepareThumbnail(QSize size,QSize frame)1943 void Gif::prepareThumbnail(QSize size, QSize frame) {
1944 	const auto document = _data;
1945 	Assert(document != nullptr);
1946 
1947 	ensureDataMediaCreated();
1948 	validateThumbnail(_dataMedia->thumbnail(), size, frame, true);
1949 	validateThumbnail(_dataMedia->thumbnailInline(), size, frame, false);
1950 }
1951 
paint(Painter & p,const QRect & clip,TextSelection selection,const PaintContext * context)1952 void Gif::paint(
1953 		Painter &p,
1954 		const QRect &clip,
1955 		TextSelection selection,
1956 		const PaintContext *context) {
1957 	const auto document = _data;
1958 	ensureDataMediaCreated();
1959 	const auto preview = Data::VideoPreviewState(_dataMedia.get());
1960 	preview.automaticLoad(getItem()->fullId());
1961 
1962 	const auto displayLoading = !preview.usingThumbnail()
1963 		&& document->displayLoading();
1964 	const auto loaded = preview.loaded();
1965 	const auto loading = preview.loading();
1966 	if (loaded
1967 		&& !_gif
1968 		&& !_gif.isBad()
1969 		&& CanPlayInline(document)) {
1970 		auto that = const_cast<Gif*>(this);
1971 		that->_gif = preview.makeAnimation([=](
1972 				Media::Clip::Notification notification) {
1973 			that->clipCallback(notification);
1974 		});
1975 	}
1976 
1977 	const auto animating = (_gif && _gif->started());
1978 	if (displayLoading) {
1979 		ensureRadial();
1980 		if (!_radial->animating()) {
1981 			_radial->start(dataProgress());
1982 		}
1983 	}
1984 	const auto radial = isRadialAnimation();
1985 
1986 	int32 height = st::inlineMediaHeight;
1987 	QSize frame = countFrameSize();
1988 
1989 	QRect r(0, 0, _width, height);
1990 	if (animating) {
1991 		const auto pixmap = _gif->current(
1992 			frame.width(),
1993 			frame.height(),
1994 			_width,
1995 			height,
1996 			ImageRoundRadius::None,
1997 			RectPart::None,
1998 			/*context->paused ? 0 : */context->ms);
1999 		if (_thumb.isNull()) {
2000 			_thumb = pixmap;
2001 			_thumbGood = true;
2002 		}
2003 		p.drawPixmap(r.topLeft(), pixmap);
2004 	} else {
2005 		prepareThumbnail({ _width, height }, frame);
2006 		if (_thumb.isNull()) {
2007 			p.fillRect(r, st::overviewPhotoBg);
2008 		} else {
2009 			p.drawPixmap(r.topLeft(), _thumb);
2010 		}
2011 	}
2012 
2013 	const auto selected = (selection == FullSelection);
2014 
2015 	if (radial
2016 		|| _gif.isBad()
2017 		|| (!_gif && !loaded && !loading && !preview.usingThumbnail())) {
2018 		const auto radialOpacity = (radial && loaded)
2019 			? _radial->opacity()
2020 			: 1.;
2021 		p.fillRect(r, st::msgDateImgBg);
2022 
2023 		p.setOpacity(radialOpacity);
2024 		auto icon = [&] {
2025 			if (radial || loading) {
2026 				return &st::historyFileInCancel;
2027 			} else if (loaded) {
2028 				return &st::historyFileInPlay;
2029 			}
2030 			return &st::historyFileInDownload;
2031 		}();
2032 		const auto size = st::overviewVideoRadialSize;
2033 		QRect inner((_width - size) / 2, (height - size) / 2, size, size);
2034 		icon->paintInCenter(p, inner);
2035 		if (radial) {
2036 			p.setOpacity(1);
2037 			const auto margin = st::msgFileRadialLine;
2038 			const auto rinner = inner
2039 				- QMargins(margin, margin, margin, margin);
2040 			auto &bg = selected
2041 				? st::historyFileInRadialFgSelected
2042 				: st::historyFileInRadialFg;
2043 			_radial->draw(p, rinner, st::msgFileRadialLine, bg);
2044 		}
2045 	}
2046 
2047 	const auto checkDelta = st::overviewCheckSkip + st::overviewCheck.size;
2048 	const auto checkLeft = _width - checkDelta;
2049 	const auto checkTop = st::overviewCheckSkip;
2050 	paintCheckbox(p, { checkLeft, checkTop }, selected, context);
2051 }
2052 
update()2053 void Gif::update() {
2054 	delegate()->repaintItem(this);
2055 }
2056 
ensureDataMediaCreated() const2057 void Gif::ensureDataMediaCreated() const {
2058 	if (_dataMedia) {
2059 		return;
2060 	}
2061 	_dataMedia = _data->createMediaView();
2062 	_dataMedia->goodThumbnailWanted();
2063 	_dataMedia->thumbnailWanted(parent()->fullId());
2064 	delegate()->registerHeavyItem(this);
2065 }
2066 
clearHeavyPart()2067 void Gif::clearHeavyPart() {
2068 	_gif.reset();
2069 	_dataMedia = nullptr;
2070 }
2071 
setPosition(int32 position)2072 void Gif::setPosition(int32 position) {
2073 	AbstractLayoutItem::setPosition(position);
2074 	if (position < 0) {
2075 		_gif.reset();
2076 	}
2077 }
2078 
dataProgress() const2079 float64 Gif::dataProgress() const {
2080 	ensureDataMediaCreated();
2081 	return _dataMedia->progress();
2082 }
2083 
dataFinished() const2084 bool Gif::dataFinished() const {
2085 	return !_data->loading();
2086 }
2087 
dataLoaded() const2088 bool Gif::dataLoaded() const {
2089 	ensureDataMediaCreated();
2090 	const auto preview = Data::VideoPreviewState(_dataMedia.get());
2091 	return preview.loaded();
2092 }
2093 
iconAnimated() const2094 bool Gif::iconAnimated() const {
2095 	return true;
2096 }
2097 
getState(QPoint point,StateRequest request) const2098 TextState Gif::getState(
2099 		QPoint point,
2100 		StateRequest request) const {
2101 	if (hasPoint(point)) {
2102 		const auto link = (_data->loading() || _data->uploading())
2103 			? _cancell
2104 			: dataLoaded()
2105 			? _openl
2106 			: _savel;
2107 		return { parent(), link };
2108 	}
2109 	return {};
2110 }
2111 
updateStatusText()2112 void Gif::updateStatusText() {
2113 	int statusSize = 0;
2114 	if (_data->status == FileDownloadFailed || _data->status == FileUploadFailed) {
2115 		statusSize = Ui::FileStatusSizeFailed;
2116 	} else if (_data->uploading()) {
2117 		statusSize = _data->uploadingData->offset;
2118 	} else if (dataLoaded()) {
2119 		statusSize = Ui::FileStatusSizeLoaded;
2120 	} else {
2121 		statusSize = Ui::FileStatusSizeReady;
2122 	}
2123 	if (statusSize != _status.size()) {
2124 		int status = statusSize, size = _data->size;
2125 		if (statusSize >= 0 && statusSize < 0x7F000000) {
2126 			size = status;
2127 			status = Ui::FileStatusSizeReady;
2128 		}
2129 		_status.update(status, size, -1, 0);
2130 		_status.setSize(statusSize);
2131 	}
2132 }
2133 
2134 } // namespace Layout
2135 } // namespace Overview
2136