From b9b7d9e33709daec348c51e81607e47c56d6e3bc Mon Sep 17 00:00:00 2001 From: John Preston <johnprestonmail@gmail.com> Date: Tue, 30 Apr 2024 14:58:32 +0400 Subject: [PATCH] Implement live location view. --- .../icons/chat/live_location_long.png | Bin 0 -> 416 bytes .../icons/chat/live_location_long@2x.png | Bin 0 -> 913 bytes .../icons/chat/live_location_long@3x.png | Bin 0 -> 1588 bytes Telegram/Resources/langs/lang.strings | 9 + .../SourceFiles/data/data_media_types.cpp | 51 ++- Telegram/SourceFiles/data/data_media_types.h | 18 +- Telegram/SourceFiles/history/history_item.cpp | 4 +- .../view/media/history_view_location.cpp | 409 ++++++++++++++++-- .../view/media/history_view_location.h | 33 +- .../history/view/media/history_view_media.cpp | 4 + .../history/view/media/history_view_media.h | 1 + Telegram/SourceFiles/ui/chat/chat.style | 6 + Telegram/SourceFiles/ui/chat/chat_style.cpp | 6 + Telegram/SourceFiles/ui/chat/chat_style.h | 1 + Telegram/build/prepare/prepare.py | 76 +++- 15 files changed, 555 insertions(+), 63 deletions(-) create mode 100644 Telegram/Resources/icons/chat/live_location_long.png create mode 100644 Telegram/Resources/icons/chat/live_location_long@2x.png create mode 100644 Telegram/Resources/icons/chat/live_location_long@3x.png 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 0000000000000000000000000000000000000000..847930f549c6a70c90798213cbfdc6becb5414f6 GIT binary patch literal 416 zcmV;R0bl-!P)<h;3K|Lk000e1NJLTq000~S000~a0ssI2{clLa00009a7bBm000XU z000XU0RWnu7ytkPQb|NXR7i>Kk-zFeVHkkl4mpwhD`tbFT!3US%YbWe0jHGgR;3hK zT!f8HXCadO36nC|+<=_)7{2-?2FLt6&-OljJ>Ofeh=YT}2b!k&{r*qycDsE(-_JQ+ z*W2y(?REpk<8eNp*ECH;qS0u--?uCa7!HT2RO*KX=kqymxm;GO6#$4xC=|M0ufX|y z-fT9&{eDj*61Jj-VE~;@M~#(ArCzU#$bP>Es@1CUrBVr)&1SZu>-Ac#_DZkU1Deg| zt29j$2#3SpqucHFEt<>afWzVN_Hr_r0Fg-Kd$d-o0j*X`jhRg5@pu5SSgc$wHyRD) zi^U?aSS-F5Ma1QDEtgB+bUIC^Q+239p&%l=-3~Y&kLr?^WhIkITTv0wb-ml|ss&Bc z%x1I7$K&zAV4%JkjYjEo+HUa=kH-@X20y*m>kR|~Kj$1A{sB+<w7b<9L_kLX0000< KMNUMnLSTYS?!6lT literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d4c0182008a996804ac48b4c201e86fc628f551f GIT binary patch literal 913 zcmV;C18)3@P)<h;3K|Lk000e1NJLTq001}u001}$0ssI2C*-V200009a7bBm000XU z000XU0RWnu7ytkRLrFwIRA_<imO&^yQ5?s|Y`curOe7^a7)C5@q2{92$+}Pu4pNkw zlqm<<rJS~i8wUr{W@C3L%E7@ZCwr1DrQNimrnIpvk~MZx)B7J@|M%Z4V{G$xdY{wm z+t2)d-`?)~%@70zgTY`h7z_r3!TdU=yzeY7E@l`;RaI40_4qVRmz0#ezrO<j@@Q9( z$321|baZqyG&ImOeSd!+i^X<#cMlH_Kb5Uks~`wG&)aM^ilP#UL^K-p`~9b<r+Kv2 zOLup7BoYBRsH(cZzu(&0`oR}PF&>ZS`Y$gpUayy7nE#Tpv$Io{<=n1_+uPfwrY7Xi z&(CYf0Dw>^WVKoiPRViH<KrXhadL7pH#g^UxmH(KB}qd2R99CA{e^{v>`X^TN3*lD zZnxX#^QBTLwC?x&4Gy}ov4N`3%*<r(M0Iub_VzXaU~Fs*t_%$gAz7B?{{DWG>EEff z+wB(@7f5JtZ#OI@%d(kFMq>{sit6d<u~;kwLA12AD2f8*o12?YbJWz-B$G+#_<TOY zg7)?G!J?T=hT}MG`S|!4?bhJn;8&W*<AF{j5}|3@n4nWrQ>du6IXO9rQU(G64b$m# zLQRsS=H}*aGmv_M78K|jw>F#2<MF`jySuyb@o^0q{2luwNs{FEF?0o$Bnd@fS@u)M zWHPO-tx*&OKMoHMr&1}6nmo_Li-(7Y>+5ShF}|m^wicZauh*L^kR-XZw3Pd!^E}_x z)s^d4RaGSt2`I0xuN#vRvbnj5a=6{@>_0<gWo0lJMBkpBofQ=o5d^_73_RKc0|VKm zIF5_OV#s$m97YChY-~)Y)2Mnl9G;$@76f5&aq;Tv3azKpX*3pAR#wpZ!NI}A#Kg$R z$nx?s8cYCyt*tF1Q$n0h=iA#G_?mb;-qzNJe3#3mAp-zHp^#yN=>xsJz0qj&tFCst z{ev%xB6`4PqbN!s5GXAz)!+KZuXz@WWps4Z;c(Q~*B2HRK0Q5MUS96(?CkCBed?E& zmy4pv^E}J4X0th&OrD>g2ZO<IIQ*l&`Mi%p%F4>jX0t5IudlCq{ECVSk|dv>pA|*P nyD0{P!C){L3<iV2{3?F{foZu_t%C1000000NkvXXu0mjfp}n~Z literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3a8cd36b53d105ad14199abfbe61d56ff057b0c8 GIT binary patch literal 1588 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 literal 0 HcmV?d00001 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<HistoryView::Media> MediaContact::createView( MediaLocation::MediaLocation( not_null<HistoryItem*> 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<HistoryItem*> 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<Media> MediaLocation::clone(not_null<HistoryItem*> parent) { return std::make_unique<MediaLocation>( + 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<HistoryView::Media> MediaLocation::createView( not_null<HistoryView::Element*> message, not_null<HistoryItem*> realParent, HistoryView::Element *replacing) { - return std::make_unique<HistoryView::Location>( - message, - _location, - _point, - _title, - _description); + return _livePeriod + ? std::make_unique<HistoryView::Location>( + message, + _location, + _point, + replacing, + _livePeriod) + : std::make_unique<HistoryView::Location>( + message, + _location, + _point, + _title, + _description); } MediaCall::MediaCall(not_null<HistoryItem*> 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<HistoryItem*> parent, - const LocationPoint &point); + const LocationPoint &point, + TimeId livePeriod = 0); MediaLocation( not_null<HistoryItem*> parent, const LocationPoint &point, @@ -356,9 +360,21 @@ public: not_null<HistoryItem*> realParent, HistoryView::Element *replacing = nullptr) override; + MediaLocation( + PrivateTag, + not_null<HistoryItem*> parent, + const LocationPoint &point, + TimeId livePeriod, + const QString &title, + const QString &description); + private: + + [[nodiscard]] QString typeString() const; + LocationPoint _point; not_null<CloudImage*> _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<Data::Media> 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<Data::MediaLocation>( 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<TimeId>::max(); +constexpr auto kLiveElapsedPartOpacity = 0.2; + +[[nodiscard]] TimeId ResolveUpdateDate(not_null<Element*> view) { + const auto item = view->data(); + const auto edited = item->Get<HistoryMessageEdited>(); + return edited ? edited->date : item->date(); +} + +[[nodiscard]] QString RemainingTimeText( + not_null<Element*> 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<Element*> 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<Element*> parent, + not_null<Data::CloudImage*> 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<LocationClickHandler>(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<Element*> 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::Live> Location::CreateLiveTracker( + not_null<Element*> 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<Live>(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<Element*> parent, not_null<Data::CloudImage*> data, Data::LocationPoint point, - const QString &title = QString(), - const QString &description = QString()); + Element *replacing = nullptr, + TimeId livePeriod = 0); + Location( + not_null<Element*> parent, + not_null<Data::CloudImage*> 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<Live> CreateLiveTracker( + not_null<Element*> 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::CloudImage*> _data; + mutable std::unique_ptr<Live> _live; mutable std::shared_ptr<QImage> _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<StickerPlayer> 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<StickerPlayer> stickerTakePlayer( not_null<DocumentData*> 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<ColorIndicesCompressed> 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<Text::QuotePaintCache>, 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