diff --git a/Telegram/Resources/icons/chat/live_location_long.png b/Telegram/Resources/icons/chat/live_location_long.png new file mode 100644 index 000000000..847930f54 Binary files /dev/null and b/Telegram/Resources/icons/chat/live_location_long.png differ diff --git a/Telegram/Resources/icons/chat/live_location_long@2x.png b/Telegram/Resources/icons/chat/live_location_long@2x.png new file mode 100644 index 000000000..d4c018200 Binary files /dev/null and b/Telegram/Resources/icons/chat/live_location_long@2x.png differ diff --git a/Telegram/Resources/icons/chat/live_location_long@3x.png b/Telegram/Resources/icons/chat/live_location_long@3x.png new file mode 100644 index 000000000..3a8cd36b5 Binary files /dev/null and b/Telegram/Resources/icons/chat/live_location_long@3x.png differ diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 52c9ed8d3..d0a709c6d 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3092,6 +3092,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_unread_bar_some" = "Unread messages"; "lng_maps_point" = "Location"; +"lng_live_location" = "Live Location"; +"lng_live_location_now" = "updated just now"; +"lng_live_location_minutes#one" = "updated {count} minute ago"; +"lng_live_location_minutes#other" = "updated {count} minutes ago"; +"lng_live_location_hours#one" = "updated {count} hour ago"; +"lng_live_location_hours#other" = "updated {count} hours ago"; +"lng_live_location_today" = "updated today at {time}"; +"lng_live_location_yesterday" = "updated yesterday at {time}"; +"lng_live_location_date_time" = "updated {date} at {time}"; "lng_save_photo" = "Save image"; "lng_save_video" = "Save video"; "lng_save_audio_file" = "Save audio file"; diff --git a/Telegram/SourceFiles/data/data_media_types.cpp b/Telegram/SourceFiles/data/data_media_types.cpp index e6dd5006a..50e063da5 100644 --- a/Telegram/SourceFiles/data/data_media_types.cpp +++ b/Telegram/SourceFiles/data/data_media_types.cpp @@ -1311,8 +1311,9 @@ std::unique_ptr MediaContact::createView( MediaLocation::MediaLocation( not_null parent, - const LocationPoint &point) -: MediaLocation(parent, point, QString(), QString()) { + const LocationPoint &point, + TimeId livePeriod) +: MediaLocation({}, parent, point, livePeriod, QString(), QString()) { } MediaLocation::MediaLocation( @@ -1320,17 +1321,30 @@ MediaLocation::MediaLocation( const LocationPoint &point, const QString &title, const QString &description) +: MediaLocation({}, parent, point, TimeId(), title, description) { +} + +MediaLocation::MediaLocation( + PrivateTag, + not_null parent, + const LocationPoint &point, + TimeId livePeriod, + const QString &title, + const QString &description) : Media(parent) , _point(point) , _location(parent->history()->owner().location(point)) +, _livePeriod(livePeriod) , _title(title) , _description(description) { } std::unique_ptr MediaLocation::clone(not_null parent) { return std::make_unique( + PrivateTag(), parent, _point, + _livePeriod, _title, _description); } @@ -1339,8 +1353,14 @@ CloudImage *MediaLocation::location() const { return _location; } +QString MediaLocation::typeString() const { + return _livePeriod + ? tr::lng_live_location(tr::now) + : tr::lng_maps_point(tr::now); +} + ItemPreview MediaLocation::toPreview(ToPreviewOptions options) const { - const auto type = tr::lng_maps_point(tr::now); + const auto type = typeString(); const auto hasMiniImages = false; const auto text = TextWithEntities{ .text = _title }; return { @@ -1349,9 +1369,7 @@ ItemPreview MediaLocation::toPreview(ToPreviewOptions options) const { } TextWithEntities MediaLocation::notificationText() const { - return WithCaptionNotificationText( - tr::lng_maps_point(tr::now), - { .text = _title }); + return WithCaptionNotificationText(typeString(), { .text = _title }); } QString MediaLocation::pinnedTextSubstring() const { @@ -1360,7 +1378,7 @@ QString MediaLocation::pinnedTextSubstring() const { TextForMimeData MediaLocation::clipboardText() const { auto result = TextForMimeData::Simple( - u"[ "_q + tr::lng_maps_point(tr::now) + u" ]\n"_q); + u"[ "_q + typeString() + u" ]\n"_q); auto titleResult = TextUtilities::ParseEntities( _title, Ui::WebpageTextTitleOptions().flags); @@ -1389,12 +1407,19 @@ std::unique_ptr MediaLocation::createView( not_null message, not_null realParent, HistoryView::Element *replacing) { - return std::make_unique( - message, - _location, - _point, - _title, - _description); + return _livePeriod + ? std::make_unique( + message, + _location, + _point, + replacing, + _livePeriod) + : std::make_unique( + message, + _location, + _point, + _title, + _description); } MediaCall::MediaCall(not_null parent, const Call &call) diff --git a/Telegram/SourceFiles/data/data_media_types.h b/Telegram/SourceFiles/data/data_media_types.h index 1aae67928..c486799cd 100644 --- a/Telegram/SourceFiles/data/data_media_types.h +++ b/Telegram/SourceFiles/data/data_media_types.h @@ -331,10 +331,14 @@ private: }; class MediaLocation final : public Media { + struct PrivateTag { + }; + public: MediaLocation( not_null parent, - const LocationPoint &point); + const LocationPoint &point, + TimeId livePeriod = 0); MediaLocation( not_null parent, const LocationPoint &point, @@ -356,9 +360,21 @@ public: not_null realParent, HistoryView::Element *replacing = nullptr) override; + MediaLocation( + PrivateTag, + not_null parent, + const LocationPoint &point, + TimeId livePeriod, + const QString &title, + const QString &description); + private: + + [[nodiscard]] QString typeString() const; + LocationPoint _point; not_null _location; + TimeId _livePeriod = 0; QString _title; QString _description; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 0695742bb..171480ab6 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -224,10 +224,12 @@ std::unique_ptr HistoryItem::CreateMedia( return nullptr; }); }, [&](const MTPDmessageMediaGeoLive &media) -> Result { + const auto period = media.vperiod().v; return media.vgeo().match([&](const MTPDgeoPoint &point) -> Result { return std::make_unique( item, - Data::LocationPoint(point)); + Data::LocationPoint(point), + media.vperiod().v); }, [](const MTPDgeoPointEmpty &) -> Result { return nullptr; }); diff --git a/Telegram/SourceFiles/history/view/media/history_view_location.cpp b/Telegram/SourceFiles/history/view/media/history_view_location.cpp index 5fab03d15..f16d65141 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_location.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_location.cpp @@ -7,12 +7,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/view/media/history_view_location.h" +#include "base/unixtime.h" #include "history/history.h" #include "history/history_item_components.h" #include "history/history_item.h" #include "history/history_location_manager.h" #include "history/view/history_view_element.h" #include "history/view/history_view_cursor_state.h" +#include "lang/lang_keys.h" #include "ui/chat/chat_style.h" #include "ui/image/image.h" #include "ui/text/text_options.h" @@ -24,6 +26,98 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_chat.h" namespace HistoryView { +namespace { + +constexpr auto kUntilOffPeriod = std::numeric_limits::max(); +constexpr auto kLiveElapsedPartOpacity = 0.2; + +[[nodiscard]] TimeId ResolveUpdateDate(not_null view) { + const auto item = view->data(); + const auto edited = item->Get(); + return edited ? edited->date : item->date(); +} + +[[nodiscard]] QString RemainingTimeText( + not_null view, + TimeId period) { + if (period == kUntilOffPeriod) { + return QString(1, QChar(0x221E)); + } + const auto elapsed = base::unixtime::now() - view->data()->date(); + const auto remaining = std::clamp(period - elapsed, 0, period); + if (remaining < 10) { + return tr::lng_seconds_tiny(tr::now, lt_count, remaining); + } else if (remaining < 600) { + return tr::lng_minutes_tiny(tr::now, lt_count, remaining / 60); + } else if (remaining < 3600) { + return QString::number(remaining / 60); + } else if (remaining < 86400) { + return tr::lng_hours_tiny(tr::now, lt_count, remaining / 3600); + } + return tr::lng_days_tiny(tr::now, lt_count, remaining / 86400); +} + +[[nodiscard]] float64 RemainingTimeProgress( + not_null view, + TimeId period) { + if (period == kUntilOffPeriod) { + return 1.; + } else if (period < 1) { + return 0.; + } + const auto elapsed = base::unixtime::now() - view->data()->date(); + return std::clamp(period - elapsed, 0, period) / float64(period); +} + +} // namespace + +struct Location::Live { + explicit Live(TimeId period) : period(period) { + } + + base::Timer updateStatusTimer; + base::Timer updateRemainingTimer; + QImage previous; + QImage previousCache; + Ui::BubbleRounding previousRounding; + Ui::Animations::Simple crossfade; + TimeId period = 0; + int thumbnailHeight = 0; +}; + +Location::Location( + not_null parent, + not_null data, + Data::LocationPoint point, + Element *replacing, + TimeId livePeriod) +: Media(parent) +, _data(data) +, _live(CreateLiveTracker(parent, livePeriod)) +, _title(st::msgMinWidth) +, _description(st::msgMinWidth) +, _link(std::make_shared(point)) { + if (_live) { + _title.setText( + st::webPageTitleStyle, + tr::lng_live_location(tr::now), + Ui::WebpageTextTitleOptions()); + _live->updateStatusTimer.setCallback([=] { + updateLiveStatus(); + checkLiveFinish(); + }); + _live->updateRemainingTimer.setCallback([=] { + checkLiveFinish(); + }); + updateLiveStatus(); + if (const auto media = replacing ? replacing->media() : nullptr) { + _live->previous = media->locationTakeImage(); + if (!_live->previous.isNull()) { + history()->owner().registerHeavyViewPart(_parent); + } + } + } +} Location::Location( not_null parent, @@ -53,18 +147,106 @@ Location::Location( } Location::~Location() { - if (_media) { - _media = nullptr; + if (hasHeavyPart()) { + unloadHeavyPart(); _parent->checkHeavyPart(); } } +void Location::checkLiveFinish() { + Expects(_live != nullptr); + + const auto now = base::unixtime::now(); + const auto item = _parent->data(); + const auto start = item->date(); + if (_live->period != kUntilOffPeriod && now - start >= _live->period) { + _live = nullptr; + item->history()->owner().requestViewResize(_parent); + } else { + _parent->repaint(); + } +} + +std::unique_ptr Location::CreateLiveTracker( + not_null parent, + TimeId period) { + if (!period) { + return nullptr; + } + const auto now = base::unixtime::now(); + const auto date = parent->data()->date(); + return (now < date || now - date < period) + ? std::make_unique(period) + : nullptr; +} + +void Location::updateLiveStatus() { + const auto date = ResolveUpdateDate(_parent); + const auto now = base::unixtime::now(); + const auto elapsed = now - date; + auto next = TimeId(); + const auto text = [&] { + if (elapsed < 60) { + next = 60 - elapsed; + return tr::lng_live_location_now(tr::now); + } else if (const auto minutes = elapsed / 60; minutes < 60) { + next = 60 - (elapsed % 60); + return tr::lng_live_location_minutes(tr::now, lt_count, minutes); + } else if (const auto hours = elapsed / 3600; hours < 12) { + next = 3600 - (elapsed % 3600); + return tr::lng_live_location_hours(tr::now, lt_count, hours); + } + const auto dateFull = base::unixtime::parse(date); + const auto nowFull = base::unixtime::parse(now); + const auto nextTomorrow = [&] { + const auto tomorrow = nowFull.date().addDays(1); + next = nowFull.secsTo(QDateTime(tomorrow, QTime(0, 0))); + }; + const auto locale = QLocale(); + const auto format = QLocale::ShortFormat; + if (dateFull.date() == nowFull.date()) { + nextTomorrow(); + const auto time = locale.toString(dateFull.time(), format); + return tr::lng_live_location_today(tr::now, lt_time, time); + } else if (dateFull.date().addDays(1) == nowFull.date()) { + nextTomorrow(); + const auto time = locale.toString(dateFull.time(), format); + return tr::lng_live_location_yesterday(tr::now, lt_time, time); + } + return tr::lng_live_location_date_time( + tr::now, + lt_date, + locale.toString(dateFull.date(), format), + lt_time, + locale.toString(dateFull.time(), format)); + }(); + _description.setMarkedText( + st::webPageDescriptionStyle, + { text }, + Ui::WebpageTextDescriptionOptions()); + if (next > 0 && next < 86400) { + _live->updateStatusTimer.callOnce(next * crl::time(1000)); + } +} + +QImage Location::locationTakeImage() { + if (_media && !_media->isNull()) { + return *_media; + } else if (_live && !_live->previous.isNull()) { + return _live->previous; + } + return {}; +} + void Location::unloadHeavyPart() { _media = nullptr; + if (_live) { + _live->previous = QImage(); + } } bool Location::hasHeavyPart() const { - return (_media != nullptr); + return (_media != nullptr) || (_live && !_live->previous.isNull()); } void Location::ensureMediaCreated() const { @@ -99,8 +281,14 @@ QSize Location::countOptimalSize() { } if (!_title.isEmpty() || !_description.isEmpty()) { minHeight += st::mediaInBubbleSkip; - if (isBubbleTop()) { - minHeight += st::msgPadding.top(); + if (_live) { + if (isBubbleBottom()) { + minHeight += st::msgPadding.bottom(); + } + } else { + if (isBubbleTop()) { + minHeight += st::msgPadding.top(); + } } } } @@ -128,6 +316,9 @@ QSize Location::countCurrentSize(int newWidth) { std::min(newWidth, st::maxMediaSize)); accumulate_max(newWidth, minWidth); accumulate_max(newHeight, st::minPhotoSize); + if (_live) { + _live->thumbnailHeight = newHeight; + } if (_parent->hasBubble()) { if (!_title.isEmpty()) { newHeight += qMin(_title.countHeight(newWidth - st::msgPadding.left() - st::msgPadding.right()), st::webPageTitleFont->height * 2); @@ -137,8 +328,14 @@ QSize Location::countCurrentSize(int newWidth) { } if (!_title.isEmpty() || !_description.isEmpty()) { newHeight += st::mediaInBubbleSkip; - if (isBubbleTop()) { - newHeight += st::msgPadding.top(); + if (_live) { + if (isBubbleBottom()) { + newHeight += st::msgPadding.bottom(); + } + } else { + if (isBubbleTop()) { + newHeight += st::msgPadding.top(); + } } } } @@ -156,20 +353,27 @@ TextSelection Location::fromDescriptionSelection( } void Location::draw(Painter &p, const PaintContext &context) const { - if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return; + if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) { + return; + } auto paintx = 0, painty = 0, paintw = width(), painth = height(); bool bubble = _parent->hasBubble(); const auto st = context.st; const auto stm = context.messageStyle(); const auto hasText = !_title.isEmpty() || !_description.isEmpty(); - const auto rounding = adjustedBubbleRounding( - hasText ? RectPart::FullTop : RectPart()); - if (bubble) { - if (hasText) { - if (isBubbleTop()) { - painty += st::msgPadding.top(); - } + const auto rounding = adjustedBubbleRounding(_live + ? RectPart::FullBottom + : hasText + ? RectPart::FullTop + : RectPart()); + const auto paintText = [&] { + if (_live) { + painty += st::mediaInBubbleSkip; + } else if (!hasText) { + return; + } else if (isBubbleTop()) { + painty += st::msgPadding.top(); } auto textw = width() - st::msgPadding.left() - st::msgPadding.right(); @@ -180,24 +384,45 @@ void Location::draw(Painter &p, const PaintContext &context) const { painty += qMin(_title.countHeight(textw), 2 * st::webPageTitleFont->height); } if (!_description.isEmpty()) { + if (_live) { + p.setPen(stm->msgDateFg); + } _description.drawLeftElided(p, paintx + st::msgPadding.left(), painty, textw, width(), 3, style::al_left, 0, -1, 0, false, toDescriptionSelection(context.selection)); painty += qMin(_description.countHeight(textw), 3 * st::webPageDescriptionFont->height); } - if (!_title.isEmpty() || !_description.isEmpty()) { + if (!_live) { painty += st::mediaInBubbleSkip; + painth -= painty; } - painth -= painty; + }; + if (!_live) { + paintText(); } - auto rthumb = QRect(paintx, painty, paintw, painth); + const auto thumbh = _live ? _live->thumbnailHeight : painth; + auto rthumb = QRect(paintx, painty, paintw, thumbh); if (!bubble) { fillImageShadow(p, rthumb, rounding, context); } ensureMediaCreated(); validateImageCache(rthumb.size(), rounding); - if (!_imageCache.isNull()) { + const auto paintPrevious = _live && !_live->previous.isNull(); + auto opacity = _imageCache.isNull() ? 0. : 1.; + if (paintPrevious) { + opacity = _live->crossfade.value(opacity); + if (opacity < 1.) { + p.drawImage(rthumb.topLeft(), _live->previousCache); + if (opacity > 0.) { + p.setOpacity(opacity); + } + } + } + if (!_imageCache.isNull() && opacity > 0.) { p.drawImage(rthumb.topLeft(), _imageCache); - } else if (!bubble) { + if (opacity < 1.) { + p.setOpacity(1.); + } + } else if (!bubble && !paintPrevious) { Ui::PaintBubble( p, Ui::SimpleBubble{ @@ -223,8 +448,12 @@ void Location::draw(Painter &p, const PaintContext &context) const { if (context.selected()) { fillImageOverlay(p, rthumb, rounding, context); } - - if (_parent->media() == this) { + if (_live) { + painty += _live->thumbnailHeight; + painth -= _live->thumbnailHeight; + paintLiveRemaining(p, context, { paintx, painty, paintw, painth }); + paintText(); + } else if (_parent->media() == this) { auto fullRight = paintx + paintw; auto fullBottom = height(); _parent->drawInfo( @@ -244,25 +473,114 @@ void Location::draw(Painter &p, const PaintContext &context) const { } } +void Location::paintLiveRemaining( + QPainter &p, + const PaintContext &context, + QRect bottom) const { + const auto size = st::liveLocationRemainingSize; + const auto skip = (bottom.height() - size) / 2; + const auto rect = QRect( + bottom.x() + bottom.width() - size - skip, + bottom.y() + skip, + size, + size); + auto hq = PainterHighQualityEnabler(p); + const auto stm = context.messageStyle(); + const auto color = stm->msgServiceFg->c; + const auto untiloff = (_live->period == kUntilOffPeriod); + const auto progress = RemainingTimeProgress(_parent, _live->period); + const auto part = 1. / 360; + const auto full = (progress >= 1. - part); + auto elapsed = color; + if (!full) { + elapsed.setAlphaF(elapsed.alphaF() * kLiveElapsedPartOpacity); + } + auto pen = QPen(elapsed); + const auto stroke = style::ConvertScaleExact(2.); + pen.setWidthF(stroke); + p.setPen(pen); + p.setBrush(Qt::NoBrush); + p.drawEllipse(rect); + + if (untiloff) { + stm->liveLocationLongIcon.paintInCenter(p, rect); + } else { + if (!full && progress > part) { + auto pen = QPen(color); + pen.setWidthF(stroke); + p.setPen(pen); + p.drawArc(rect, 90 * 16, int(base::SafeRound(360 * 16 * progress))); + } + + p.setPen(stm->msgServiceFg); + p.setFont(st::semiboldFont); + const auto text = RemainingTimeText(_parent, _live->period); + p.drawText(rect, text, style::al_center); + const auto each = std::clamp(_live->period / 360, 1, 86400); + _live->updateRemainingTimer.callOnce(each * crl::time(1000)); + } +} + void Location::validateImageCache( QSize outer, Ui::BubbleRounding rounding) const { Expects(_media != nullptr); - const auto ratio = style::DevicePixelRatio(); - if ((_imageCache.size() == (outer * ratio) - && _imageCacheRounding == rounding) - || _media->isNull()) { + if (_live && !_live->previous.isNull()) { + validateImageCache( + _live->previous, + _live->previousCache, + _live->previousRounding, + outer, + rounding); + } + validateImageCache( + *_media, + _imageCache, + _imageCacheRounding, + outer, + rounding); + checkLiveCrossfadeStart(); +} + +void Location::checkLiveCrossfadeStart() const { + if (!_live + || _live->previous.isNull() + || !_media + || _media->isNull() + || _live->crossfade.animating()) { return; } - _imageCache = Images::Round( - _media->scaled( + _live->crossfade.start([=] { + if (!_live->crossfade.animating()) { + _live->previous = QImage(); + _live->previousCache = QImage(); + } + _parent->repaint(); + }, 0., 1., st::fadeWrapDuration); +} + +void Location::validateImageCache( + const QImage &source, + QImage &cache, + Ui::BubbleRounding &cacheRounding, + QSize outer, + Ui::BubbleRounding rounding) const { + if (source.isNull()) { + return; + } + const auto ratio = style::DevicePixelRatio(); + if (cache.size() == (outer * ratio) && cacheRounding == rounding) { + return; + } + cache = Images::Round( + source.scaled( outer * ratio, Qt::IgnoreAspectRatio, Qt::SmoothTransformation), MediaRoundingMask(rounding)); - _imageCache.setDevicePixelRatio(ratio); - _imageCacheRounding = rounding; + cache.setDevicePixelRatio(ratio); + cacheRounding = rounding; } TextState Location::textState(QPoint point, StateRequest request) const { @@ -275,11 +593,13 @@ TextState Location::textState(QPoint point, StateRequest request) const { auto paintx = 0, painty = 0, paintw = width(), painth = height(); bool bubble = _parent->hasBubble(); - if (bubble) { - if (!_title.isEmpty() || !_description.isEmpty()) { - if (isBubbleTop()) { - painty += st::msgPadding.top(); - } + auto checkText = [&] { + if (_live) { + painty += st::mediaInBubbleSkip; + } else if (_title.isEmpty() && _description.isEmpty()) { + return false; + } else if (isBubbleTop()) { + painty += st::msgPadding.top(); } auto textw = width() - st::msgPadding.left() - st::msgPadding.right(); @@ -292,7 +612,7 @@ TextState Location::textState(QPoint point, StateRequest request) const { textw, width(), request.forText())); - return result; + return true; } else if (point.y() >= painty + titleh) { symbolAdd += _title.length(); } @@ -306,6 +626,8 @@ TextState Location::textState(QPoint point, StateRequest request) const { textw, width(), request.forText())); + result.symbol += symbolAdd; + return true; } else if (point.y() >= painty + descriptionh) { symbolAdd += _description.length(); } @@ -315,11 +637,22 @@ TextState Location::textState(QPoint point, StateRequest request) const { painty += st::mediaInBubbleSkip; } painth -= painty; + return false; + }; + if (!_live && checkText()) { + return result; } - if (QRect(paintx, painty, paintw, painth).contains(point) && _data) { + const auto thumbh = _live ? _live->thumbnailHeight : painth; + if (QRect(paintx, painty, paintw, thumbh).contains(point) && _data) { result.link = _link; } - if (_parent->media() == this) { + if (_live) { + painty += _live->thumbnailHeight; + painth -= _live->thumbnailHeight; + if (checkText()) { + return result; + } + } else if (_parent->media() == this) { auto fullRight = paintx + paintw; auto fullBottom = height(); const auto bottomInfoResult = _parent->bottomInfoTextState( diff --git a/Telegram/SourceFiles/history/view/media/history_view_location.h b/Telegram/SourceFiles/history/view/media/history_view_location.h index ec412fd00..9e431ef74 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_location.h +++ b/Telegram/SourceFiles/history/view/media/history_view_location.h @@ -22,8 +22,14 @@ public: not_null parent, not_null data, Data::LocationPoint point, - const QString &title = QString(), - const QString &description = QString()); + Element *replacing = nullptr, + TimeId livePeriod = 0); + Location( + not_null parent, + not_null data, + Data::LocationPoint point, + const QString &title, + const QString &description); ~Location(); void draw(Painter &p, const PaintContext &context) const override; @@ -58,15 +64,33 @@ public: return isRoundedInBubbleBottom(); } + QImage locationTakeImage() override; + void unloadHeavyPart() override; bool hasHeavyPart() const override; private: + struct Live; + [[nodiscard]] static std::unique_ptr CreateLiveTracker( + not_null parent, + TimeId period); + void ensureMediaCreated() const; void validateImageCache( QSize outer, Ui::BubbleRounding rounding) const; + void validateImageCache( + const QImage &source, + QImage &cache, + Ui::BubbleRounding &cacheRounding, + QSize outer, + Ui::BubbleRounding rounding) const; + + void paintLiveRemaining( + QPainter &p, + const PaintContext &context, + QRect bottom) const; QSize countOptimalSize() override; QSize countCurrentSize(int newWidth) override; @@ -79,7 +103,12 @@ private: [[nodiscard]] int fullWidth() const; [[nodiscard]] int fullHeight() const; + void checkLiveCrossfadeStart() const; + void updateLiveStatus(); + void checkLiveFinish(); + const not_null _data; + mutable std::unique_ptr _live; mutable std::shared_ptr _media; Ui::Text::String _title, _description; ClickHandlerPtr _link; diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.cpp b/Telegram/SourceFiles/history/view/media/history_view_media.cpp index f64cb808b..56720e83f 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_media.cpp @@ -356,6 +356,10 @@ std::unique_ptr Media::stickerTakePlayer( return nullptr; } +QImage Media::locationTakeImage() { + return QImage(); +} + TextState Media::getStateGrouped( const QRect &geometry, RectParts sides, diff --git a/Telegram/SourceFiles/history/view/media/history_view_media.h b/Telegram/SourceFiles/history/view/media/history_view_media.h index 1b835dcce..f9ad5c987 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_media.h +++ b/Telegram/SourceFiles/history/view/media/history_view_media.h @@ -190,6 +190,7 @@ public: virtual std::unique_ptr stickerTakePlayer( not_null data, const Lottie::ColorReplacements *replacements); + virtual QImage locationTakeImage(); virtual void checkAnimation() { } diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 2cab84839..5dd77998a 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -1070,3 +1070,9 @@ chatIntroWidth: 224px; chatIntroTitleMargin: margins(11px, 16px, 11px, 4px); chatIntroMargin: margins(11px, 0px, 11px, 0px); chatIntroStickerPadding: margins(10px, 8px, 10px, 16px); + +liveLocationLongInIcon: icon {{ "chat/live_location_long", msgInServiceFg }}; +liveLocationLongInIconSelected: icon {{ "chat/live_location_long", msgInServiceFgSelected }}; +liveLocationLongOutIcon: icon {{ "chat/live_location_long", msgOutServiceFg }}; +liveLocationLongOutIconSelected: icon {{ "chat/live_location_long", msgOutServiceFgSelected }}; +liveLocationRemainingSize: 28px; diff --git a/Telegram/SourceFiles/ui/chat/chat_style.cpp b/Telegram/SourceFiles/ui/chat/chat_style.cpp index 9470d71d6..a532a3180 100644 --- a/Telegram/SourceFiles/ui/chat/chat_style.cpp +++ b/Telegram/SourceFiles/ui/chat/chat_style.cpp @@ -543,6 +543,12 @@ ChatStyle::ChatStyle(rpl::producer colorIndices) { st::historyVoiceMessageInTTLSelected, st::historyVoiceMessageOutTTL, st::historyVoiceMessageOutTTLSelected); + make( + &MessageStyle::liveLocationLongIcon, + st::liveLocationLongInIcon, + st::liveLocationLongInIconSelected, + st::liveLocationLongOutIcon, + st::liveLocationLongOutIconSelected); updateDarkValue(); } diff --git a/Telegram/SourceFiles/ui/chat/chat_style.h b/Telegram/SourceFiles/ui/chat/chat_style.h index 1053d630a..6b8cc459b 100644 --- a/Telegram/SourceFiles/ui/chat/chat_style.h +++ b/Telegram/SourceFiles/ui/chat/chat_style.h @@ -92,6 +92,7 @@ struct MessageStyle { style::icon historyTranscribeLock = { Qt::Uninitialized }; style::icon historyTranscribeHide = { Qt::Uninitialized }; style::icon historyVoiceMessageTTL = { Qt::Uninitialized }; + style::icon liveLocationLongIcon = { Qt::Uninitialized }; std::array< std::unique_ptr, kColorPatternsCount> quoteCache; diff --git a/Telegram/build/prepare/prepare.py b/Telegram/build/prepare/prepare.py index ce00e278e..6351f95c6 100644 --- a/Telegram/build/prepare/prepare.py +++ b/Telegram/build/prepare/prepare.py @@ -1510,22 +1510,82 @@ mac: """) if buildQt6: - stage('qt_6_2_8', """ + qt6version = '6.2.8' if mac else '6.7.0' + qt6version_ = '6_2_8' if mac else '6_7_0' + arg0 = 'qt_' + qt6version_ + arg1 = qt6version + + stage('qt_' + qt6version_, """ +win: + git clone -b v{1} https://github.com/qt/qt5.git {0} mac: - git clone -b v6.2.8-lts-lgpl https://github.com/qt/qt5.git qt_6_2_8 - cd qt_6_2_8 + git clone -b v{1}-lts-lgpl https://github.com/qt/qt5.git {0} +common: + cd {0} git submodule update --init --recursive qtbase qtimageformats qtsvg -depends:patches/qtbase_6.2.8/*.patch +depends:patches/qtbase_{1}/*.patch cd qtbase - find ../../patches/qtbase_6.2.8 -type f -print0 | sort -z | xargs -0 git apply -v +win: + for /r %%i in (..\\..\\patches\\qtbase_{1}\\*) do git apply %%i -v cd .. - sed -i.bak 's/tqtc-//' {qtimageformats,qtsvg}/dependencies.yaml + + SET CONFIGURATIONS=-debug +release: + SET CONFIGURATIONS=-debug-and-release +win: + """.format(arg0, arg1) + removeDir("\"%LIBS_DIR%\\Qt-\"" + qt6version) + """ + SET ANGLE_DIR=%LIBS_DIR%\\tg_angle + SET ANGLE_LIBS_DIR=%ANGLE_DIR%\\out + SET MOZJPEG_DIR=%LIBS_DIR%\\mozjpeg + SET OPENSSL_DIR=%LIBS_DIR%\\openssl3 + SET OPENSSL_LIBS_DIR=%OPENSSL_DIR%\\out + SET ZLIB_LIBS_DIR=%LIBS_DIR%\\zlib + SET WEBP_DIR=%LIBS_DIR%\\libwebp + configure -prefix "%LIBS_DIR%\\Qt-{1}" ^ + %CONFIGURATIONS% ^ + -force-debug-info ^ + -opensource ^ + -confirm-license ^ + -static ^ + -static-runtime ^ + -opengl es2 -no-angle ^ + -I "%ANGLE_DIR%\\include" ^ + -D "KHRONOS_STATIC=" ^ + -D "DESKTOP_APP_QT_STATIC_ANGLE=" ^ + QMAKE_LIBS_OPENGL_ES2_DEBUG="%ANGLE_LIBS_DIR%\\Debug\\tg_angle.lib %ZLIB_LIBS_DIR%\\Debug\\zlibstaticd.lib d3d9.lib dxgi.lib dxguid.lib" ^ + QMAKE_LIBS_OPENGL_ES2_RELEASE="%ANGLE_LIBS_DIR%\\Release\\tg_angle.lib %ZLIB_LIBS_DIR%\\Release\\zlibstatic.lib d3d9.lib dxgi.lib dxguid.lib" ^ + -egl ^ + QMAKE_LIBS_EGL_DEBUG="%ANGLE_LIBS_DIR%\\Debug\\tg_angle.lib %ZLIB_LIBS_DIR%\\Debug\\zlibstaticd.lib d3d9.lib dxgi.lib dxguid.lib Gdi32.lib User32.lib" ^ + QMAKE_LIBS_EGL_RELEASE="%ANGLE_LIBS_DIR%\\Release\\tg_angle.lib %ZLIB_LIBS_DIR%\\Release\\zlibstatic.lib d3d9.lib dxgi.lib dxguid.lib Gdi32.lib User32.lib" ^ + -openssl-linked ^ + -I "%OPENSSL_DIR%\\include" ^ + OPENSSL_LIBS_DEBUG="%OPENSSL_LIBS_DIR%.dbg\\libssl.lib %OPENSSL_LIBS_DIR%.dbg\\libcrypto.lib Ws2_32.lib Gdi32.lib Advapi32.lib Crypt32.lib User32.lib" ^ + OPENSSL_LIBS_RELEASE="%OPENSSL_LIBS_DIR%\\libssl.lib %OPENSSL_LIBS_DIR%\\libcrypto.lib Ws2_32.lib Gdi32.lib Advapi32.lib Crypt32.lib User32.lib" ^ + -I "%MOZJPEG_DIR%" ^ + LIBJPEG_LIBS_DEBUG="%MOZJPEG_DIR%\\Debug\\jpeg-static.lib" ^ + LIBJPEG_LIBS_RELEASE="%MOZJPEG_DIR%\\Release\\jpeg-static.lib" ^ + -system-webp ^ + -I "%WEBP_DIR%\\src" ^ + -L "%WEBP_DIR%\\out\\release-static\\$X8664\\lib" ^ + -mp ^ + -no-feature-netlistmgr ^ + -nomake examples ^ + -nomake tests ^ + -platform win32-msvc + + jom -j%NUMBER_OF_PROCESSORS% + jom -j%NUMBER_OF_PROCESSORS% install + +mac: + find ../../patches/qtbase_{1} -type f -print0 | sort -z | xargs -0 git apply -v + cd .. + sed -i.bak 's/tqtc-//' {{qtimageformats,qtsvg}}/dependencies.yaml CONFIGURATIONS=-debug release: CONFIGURATIONS=-debug-and-release mac: - ./configure -prefix "$USED_PREFIX/Qt-6.2.8" \ + ./configure -prefix "$USED_PREFIX/Qt-{1}" \ $CONFIGURATIONS \ -force-debug-info \ -opensource \ @@ -1546,7 +1606,7 @@ mac: ninja ninja install -""") +""".format(arg0, arg1)) stage('tg_owt', """ git clone https://github.com/desktop-app/tg_owt.git