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