Implement weather area in stories.

This commit is contained in:
John Preston 2024-07-23 14:56:06 +02:00
parent 54ce85f8e6
commit 5fdd4eba80
8 changed files with 369 additions and 34 deletions

View file

@ -41,13 +41,10 @@ using UpdateFlag = StoryUpdate::Flag;
return {
.geometry = { corner / 100., size / 100. },
.rotation = data.vrotation().v,
.radius = data.vradius().value_or_empty(),
};
}
[[nodiscard]] uint32 ParseMilliKelvin(double celcius) {
return uint32(std::clamp(celcius + 273.15, 0., 1'000'000.) * 1000.);
}
[[nodiscard]] TextWithEntities StripLinks(TextWithEntities text) {
const auto link = [&](const EntityInText &entity) {
return (entity.type() == EntityType::CustomUrl)
@ -176,8 +173,11 @@ using UpdateFlag = StoryUpdate::Flag;
result.emplace(WeatherArea{
.area = ParseArea(data.vcoordinates()),
.emoji = qs(data.vemoji()),
.color = Ui::ColorFromSerialized(data.vcolor().v),
.millicelcius = int(data.vtemperature_c().v * 1000.),
.color = Ui::Color32FromSerialized(data.vcolor().v),
.millicelsius = int(1000. * std::clamp(
data.vtemperature_c().v,
-274.,
1'000'000.)),
});
}, [&](const MTPDinputMediaAreaChannelPost &data) {
LOG(("API Error: Unexpected inputMediaAreaChannelPost from API."));
@ -721,6 +721,10 @@ const std::vector<UrlArea> &Story::urlAreas() const {
return _urlAreas;
}
const std::vector<WeatherArea> &Story::weatherAreas() const {
return _weatherAreas;
}
void Story::applyChanges(
StoryMedia media,
const MTPDstoryItem &data,
@ -825,6 +829,7 @@ void Story::applyFields(
auto suggestedReactions = std::vector<SuggestedReaction>();
auto channelPosts = std::vector<ChannelPost>();
auto urlAreas = std::vector<UrlArea>();
auto weatherAreas = std::vector<WeatherArea>();
if (const auto areas = data.vmedia_areas()) {
for (const auto &area : areas->v) {
if (const auto location = ParseLocation(area)) {
@ -840,6 +845,8 @@ void Story::applyFields(
channelPosts.push_back(*post);
} else if (auto url = ParseUrlArea(area)) {
urlAreas.push_back(*url);
} else if (auto weather = ParseWeatherArea(area)) {
weatherAreas.push_back(*weather);
}
}
}
@ -853,6 +860,7 @@ void Story::applyFields(
= (_suggestedReactions != suggestedReactions);
const auto channelPostsChanged = (_channelPosts != channelPosts);
const auto urlAreasChanged = (_urlAreas != urlAreas);
const auto weatherAreasChanged = (_weatherAreas != weatherAreas);
const auto reactionChanged = (_sentReactionId != reaction);
_out = out;
@ -881,6 +889,9 @@ void Story::applyFields(
if (urlAreasChanged) {
_urlAreas = std::move(urlAreas);
}
if (weatherAreasChanged) {
_weatherAreas = std::move(weatherAreas);
}
if (reactionChanged) {
_sentReactionId = reaction;
}
@ -891,7 +902,8 @@ void Story::applyFields(
|| mediaChanged
|| locationsChanged
|| channelPostsChanged
|| urlAreasChanged;
|| urlAreasChanged
|| weatherAreasChanged;
const auto reactionsChanged = reactionChanged
|| suggestedReactionsChanged;
if (!initial && (changed || reactionsChanged)) {

View file

@ -81,6 +81,7 @@ struct StoryViews {
struct StoryArea {
QRectF geometry;
float64 rotation = 0;
float64 radius = 0;
friend inline bool operator==(
const StoryArea &,
@ -135,7 +136,7 @@ struct WeatherArea {
StoryArea area;
QString emoji;
QColor color;
int millicelcius = 0;
int millicelsius = 0;
friend inline bool operator==(
const WeatherArea &,
@ -219,6 +220,8 @@ public:
-> const std::vector<ChannelPost> &;
[[nodiscard]] auto urlAreas() const
-> const std::vector<UrlArea> &;
[[nodiscard]] auto weatherAreas() const
-> const std::vector<WeatherArea> &;
void applyChanges(
StoryMedia media,
@ -270,6 +273,7 @@ private:
std::vector<SuggestedReaction> _suggestedReactions;
std::vector<ChannelPost> _channelPosts;
std::vector<UrlArea> _urlAreas;
std::vector<WeatherArea> _weatherAreas;
StoryViews _views;
StoryViews _channelReactions;
const TimeId _date = 0;

View file

@ -536,8 +536,9 @@ void Controller::rebuildActiveAreas(const Layout &layout) const {
int(base::SafeRound(general.width() * scale.width())),
int(base::SafeRound(general.height() * scale.height()))
).translated(origin);
if (const auto reaction = area.reaction.get()) {
reaction->setAreaGeometry(area.geometry);
area.radius = scale.width() * area.radiusOriginal / 100.;
if (const auto view = area.view.get()) {
view->setAreaGeometry(area.geometry, area.radius);
}
}
}
@ -1050,6 +1051,9 @@ void Controller::updateAreas(Data::Story *story) {
const auto &urlAreas = story
? story->urlAreas()
: std::vector<Data::UrlArea>();
const auto &weatherAreas = story
? story->weatherAreas()
: std::vector<Data::WeatherArea>();
if (_locations != locations) {
_locations = locations;
_areas.clear();
@ -1062,13 +1066,18 @@ void Controller::updateAreas(Data::Story *story) {
_urlAreas = urlAreas;
_areas.clear();
}
if (_weatherAreas != weatherAreas) {
_weatherAreas = weatherAreas;
_areas.clear();
}
const auto reactionsCount = int(suggestedReactions.size());
if (_suggestedReactions.size() == reactionsCount && !_areas.empty()) {
for (auto i = 0; i != reactionsCount; ++i) {
const auto count = suggestedReactions[i].count;
if (_suggestedReactions[i].count != count) {
_suggestedReactions[i].count = count;
_areas[i + _locations.size()].reaction->updateCount(count);
const auto view = _areas[i + _locations.size()].view.get();
view->updateReactionsCount(count);
}
if (_suggestedReactions[i] != suggestedReactions[i]) {
_suggestedReactions = suggestedReactions;
@ -1206,7 +1215,8 @@ ClickHandlerPtr Controller::lookupAreaHandler(QPoint point) const {
|| (_locations.empty()
&& _suggestedReactions.empty()
&& _channelPosts.empty()
&& _urlAreas.empty())) {
&& _urlAreas.empty()
&& _weatherAreas.empty())) {
return nullptr;
} else if (_areas.empty()) {
const auto now = story();
@ -1240,7 +1250,7 @@ ClickHandlerPtr Controller::lookupAreaHandler(QPoint point) const {
}
}
}),
.reaction = std::move(widget),
.view = std::move(widget),
});
}
if (const auto session = now ? &now->session() : nullptr) {
@ -1261,19 +1271,27 @@ ClickHandlerPtr Controller::lookupAreaHandler(QPoint point) const {
.handler = std::make_shared<HiddenUrlClickHandler>(url.url),
});
}
for (const auto &weather : _weatherAreas) {
auto widget = _reactions->makeWeatherAreaWidget(weather);
const auto raw = widget.get();
_areas.push_back({
.original = weather.area.geometry,
.radiusOriginal = weather.area.radius,
.rotation = weather.area.rotation,
.handler = std::make_shared<LambdaClickHandler>([=] {
raw->toggleMode();
}),
.view = std::move(widget),
});
}
rebuildActiveAreas(*layout);
}
const auto circleContains = [&](QRect circle) {
const auto radius = std::min(circle.width(), circle.height()) / 2;
const auto delta = circle.center() - point;
return QPoint::dotProduct(delta, delta) < (radius * radius);
};
for (const auto &area : _areas) {
const auto center = area.geometry.center();
const auto angle = -area.rotation;
const auto contains = area.reaction
? circleContains(area.geometry)
const auto contains = area.view
? area.view->contains(point)
: area.geometry.contains(Rotated(point, center, angle));
if (contains) {
return area.handler;

View file

@ -68,7 +68,7 @@ struct ContentLayout;
class CaptionFullView;
class RepostView;
enum class ReactionsMode;
class SuggestedReactionView;
class StoryAreaView;
struct RepostClickHandler;
enum class HeaderLayout {
@ -208,10 +208,12 @@ private:
};
struct ActiveArea {
QRectF original;
float64 radiusOriginal = 0.;
QRect geometry;
float64 rotation = 0.;
float64 radius = 0.;
ClickHandlerPtr handler;
std::unique_ptr<SuggestedReactionView> reaction;
std::unique_ptr<StoryAreaView> view;
};
void initLayout();
@ -303,6 +305,7 @@ private:
std::vector<Data::SuggestedReaction> _suggestedReactions;
std::vector<Data::ChannelPost> _channelPosts;
std::vector<Data::UrlArea> _urlAreas;
std::vector<Data::WeatherArea> _weatherAreas;
mutable std::vector<ActiveArea> _areas;
std::vector<CachedSource> _cachedSourcesList;

View file

@ -11,6 +11,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/unixtime.h"
#include "boxes/premium_preview_box.h"
#include "chat_helpers/compose/compose_show.h"
#include "chat_helpers/stickers_lottie.h"
#include "chat_helpers/stickers_emoji_pack.h"
#include "data/data_changes.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
@ -20,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/admin_log/history_admin_log_item.h"
#include "history/view/media/history_view_custom_emoji.h"
#include "history/view/media/history_view_media_unwrapped.h"
#include "history/view/media/history_view_sticker_player.h"
#include "history/view/reactions/history_view_reactions_selector.h"
#include "history/view/history_view_element.h"
#include "history/history_item_reply_markup.h"
@ -61,7 +64,7 @@ constexpr auto kStoppingFadeDuration = crl::time(150);
class ReactionView final
: public Ui::RpWidget
, public SuggestedReactionView
, public StoryAreaView
, public HistoryView::DefaultElementDelegate {
public:
ReactionView(
@ -69,9 +72,11 @@ public:
not_null<Main::Session*> session,
const Data::SuggestedReaction &reaction);
void setAreaGeometry(QRect geometry) override;
void updateCount(int count) override;
void setAreaGeometry(QRect geometry, float64 radius) override;
void updateReactionsCount(int count) override;
void playEffect() override;
void toggleMode() override;
bool contains(QPoint point) override;
private:
using Element = HistoryView::Element;
@ -108,6 +113,7 @@ private:
Ui::Text::String _counter;
Ui::Animations::Simple _counterAnimation;
QRectF _bubbleGeometry;
QRect _apiGeometry;
int _size = 0;
int _mediaLeft = 0;
int _mediaTop = 0;
@ -126,6 +132,58 @@ private:
};
class WeatherView final : public Ui::RpWidget, public StoryAreaView {
public:
WeatherView(
QWidget *parent,
not_null<Main::Session*> session,
const Data::WeatherArea &data);
void setAreaGeometry(QRect geometry, float64 radius) override;
void updateReactionsCount(int count) override;
void playEffect() override;
void toggleMode() override;
bool contains(QPoint point) override;
private:
void paintEvent(QPaintEvent *e) override;
void cacheBackground();
void watchForSticker();
void setStickerFrom(not_null<DocumentData*> document);
[[nodiscard]] QSize stickerSize() const;
const not_null<Main::Session*> _session;
Data::WeatherArea _data;
EmojiPtr _emoji;
QColor _fg;
QImage _background;
QFont _font;
QRectF _rect;
QRect _wrapped;
float64 _radius = 0.;
int _emojiSize = 0;
int _padding = 0;
bool _celsius = true;
std::shared_ptr<HistoryView::StickerPlayer> _sticker;
rpl::lifetime _lifetime;
};
[[nodiscard]] QPoint Rotated(QPoint point, QPoint origin, float64 angle) {
if (std::abs(angle) < 1.) {
return point;
}
const auto alpha = angle / 180. * M_PI;
const auto acos = cos(alpha);
const auto asin = sin(alpha);
point -= origin;
return origin + QPoint(
int(base::SafeRound(acos * point.x() - asin * point.y())),
int(base::SafeRound(asin * point.x() + acos * point.y())));
}
[[nodiscard]] AdminLog::OwnedItem GenerateFakeItem(
not_null<HistoryView::ElementDelegate*> delegate,
not_null<History*> history) {
@ -140,6 +198,13 @@ private:
return AdminLog::OwnedItem(delegate, item);
}
[[nodiscard]] QColor ChooseWeatherFg(const QColor &bg) {
const auto luminance = (0.2126 * bg.redF())
+ (0.7152 * bg.greenF())
+ (0.0722 * bg.blueF());
return (luminance > 0.705) ? QColor(0, 0, 0) : QColor(255, 255, 255);
}
ReactionView::ReactionView(
QWidget *parent,
not_null<Main::Session*> session,
@ -198,7 +263,7 @@ ReactionView::ReactionView(
}, lifetime());
_data.count = 0;
updateCount(reaction.count);
updateReactionsCount(reaction.count);
_counterAnimation.stop();
setupCustomChatStylePalette();
@ -212,7 +277,8 @@ void ReactionView::setupCustomChatStylePalette() {
_chatStyle->applyCustomPalette(_chatStyle.get());
}
void ReactionView::setAreaGeometry(QRect geometry) {
void ReactionView::setAreaGeometry(QRect geometry, float64 radius) {
_apiGeometry = geometry;
_size = std::min(geometry.width(), geometry.height());
_bubble = _size * kSuggestedBubbleSize;
_bigOffset = _bubble * kSuggestedTailBigOffset;
@ -228,7 +294,7 @@ void ReactionView::setAreaGeometry(QRect geometry) {
updateEffectGeometry();
}
void ReactionView::updateCount(int count) {
void ReactionView::updateReactionsCount(int count) {
if (_data.count == count) {
return;
}
@ -283,6 +349,17 @@ void ReactionView::playEffect() {
}
}
void ReactionView::toggleMode() {
Unexpected("ReactionView::toggleMode.");
}
bool ReactionView::contains(QPoint point) {
const auto circle = _apiGeometry;
const auto radius = std::min(circle.width(), circle.height()) / 2;
const auto delta = circle.center() - point;
return QPoint::dotProduct(delta, delta) < (radius * radius);
}
void ReactionView::paintEffectFrame(
QPainter &p,
not_null<Ui::ReactionFlyAnimation*> effect,
@ -457,6 +534,205 @@ void ReactionView::cacheBackground() {
paintShape(_data.dark ? dark : QColor(255, 255, 255));
}
WeatherView::WeatherView(
QWidget *parent,
not_null<Main::Session*> session,
const Data::WeatherArea &data)
: RpWidget(parent)
, _session(session)
, _data(data)
, _emoji(Ui::Emoji::Find(_data.emoji))
, _fg(ChooseWeatherFg(_data.color)) {
watchForSticker();
setAttribute(Qt::WA_TransparentForMouseEvents);
show();
}
void WeatherView::watchForSticker() {
if (!_emoji) {
return;
}
const auto emojiStickers = &_session->emojiStickersPack();
if (const auto sticker = emojiStickers->stickerForEmoji(_emoji)) {
setStickerFrom(sticker.document);
} else {
emojiStickers->refreshed() | rpl::map([=] {
return emojiStickers->stickerForEmoji(_emoji).document;
}) | rpl::filter([=](DocumentData *document) {
return document != nullptr;
}) | rpl::take(
1
) | rpl::start_with_next([=](not_null<DocumentData*> document) {
setStickerFrom(document);
update();
}, _lifetime);
}
}
void WeatherView::setAreaGeometry(QRect geometry, float64 radius) {
const auto diagxdiag = (geometry.width() * geometry.width())
+ (geometry.height() * geometry.height());
const auto diag = std::sqrt(diagxdiag);
const auto topleft = QRectF(geometry).center()
- QPointF(diag / 2., diag / 2.);
const auto bottomright = topleft + QPointF(diag, diag);
const auto left = int(std::floor(topleft.x()));
const auto top = int(std::floor(topleft.y()));
const auto right = int(std::ceil(bottomright.x()));
const auto bottom = int(std::ceil(bottomright.y()));
setGeometry(left, top, right - left, bottom - top);
_rect = QRectF(geometry).translated(-left, -top);
_radius = radius;
_emojiSize = int(base::SafeRound(_rect.height() * 2 / 3.));
_font = st::semiboldFont->f;
_font.setPixelSize(_emojiSize);
_background = {};
}
void WeatherView::updateReactionsCount(int count) {
Unexpected("WeatherView::updateRactionsCount.");
}
void WeatherView::playEffect() {
Unexpected("WeatherView::playEffect.");
}
void WeatherView::toggleMode() {
_celsius = !_celsius;
_background = {};
update();
}
bool WeatherView::contains(QPoint point) {
const auto geometry = _rect.translated(pos()).toRect();
const auto angle = -_data.area.rotation;
return geometry.contains(Rotated(point, geometry.center(), angle));
}
void WeatherView::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
if (_background.size() != size() * style::DevicePixelRatio()) {
cacheBackground();
}
p.drawImage(0, 0, _background);
if (_sticker && _sticker->ready()) {
auto hq = PainterHighQualityEnabler(p);
const auto rcenter = _wrapped.center();
p.translate(rcenter);
p.rotate(_data.area.rotation);
p.translate(-rcenter);
const auto image = _sticker->frame(
stickerSize(),
QColor(0, 0, 0, 0),
false,
crl::now(),
false).image;
const auto size = image.size() / style::DevicePixelRatio();
const auto rect = QRectF(
_wrapped.x() + _padding + (_emojiSize - size.width()) / 2.,
_wrapped.y() + (_wrapped.height() - size.height()) / 2.,
size.width(),
size.height());
const auto scenter = rect.center();
const auto scale = (_emojiSize * 1.) / stickerSize().width();
p.translate(scenter);
p.scale(scale, scale);
p.translate(-scenter);
p.drawImage(rect, image);
_sticker->markFrameShown();
}
}
QSize WeatherView::stickerSize() const {
return QSize(st::chatIntroStickerSize, st::chatIntroStickerSize);
}
void WeatherView::setStickerFrom(not_null<DocumentData*> document) {
if (_sticker || !_emoji) {
return;
}
const auto media = document->createMediaView();
media->checkStickerLarge();
media->goodThumbnailWanted();
rpl::single() | rpl::then(
document->owner().session().downloaderTaskFinished()
) | rpl::filter([=] {
return media->loaded();
}) | rpl::take(1) | rpl::start_with_next([=] {
const auto sticker = document->sticker();
if (sticker->isLottie()) {
_sticker = std::make_shared<HistoryView::LottiePlayer>(
ChatHelpers::LottiePlayerFromDocument(
media.get(),
ChatHelpers::StickerLottieSize::StickerSet,
stickerSize(),
Lottie::Quality::High));
} else if (sticker->isWebm()) {
_sticker = std::make_shared<HistoryView::WebmPlayer>(
media->owner()->location(),
media->bytes(),
stickerSize());
} else {
_sticker = std::make_shared<HistoryView::StaticStickerPlayer>(
media->owner()->location(),
media->bytes(),
stickerSize());
}
_sticker->setRepaintCallback([=] { update(); });
update();
}, _lifetime);
}
void WeatherView::cacheBackground() {
const auto ratio = style::DevicePixelRatio();
_background = QImage(
size() * ratio,
QImage::Format_ARGB32_Premultiplied);
_background.setDevicePixelRatio(ratio);
_background.fill(Qt::transparent);
auto p = QPainter(&_background);
auto hq = PainterHighQualityEnabler(p);
p.setBrush(_data.color);
p.setPen(Qt::NoPen);
const auto center = _rect.center();
p.translate(center);
p.rotate(_data.area.rotation);
p.translate(-center);
const auto format = [](float64 value) {
return QString::number(int(base::SafeRound(value * 10)) / 10.);
};
const auto text = [&] {
const auto celsius = _data.millicelsius / 1000.;
if (_celsius) {
return format(celsius);
}
const auto fahrenheit = (celsius * 9.0 / 5.0) + 32;
return format(fahrenheit);
}().append(QChar(0xb0)).append(_celsius ? "C" : "F");
const auto metrics = QFontMetrics(_font);
const auto textWidth = metrics.horizontalAdvance(text);
_padding = int(_rect.height() / 6);
const auto fullWidth = (_emoji ? _emojiSize : 0)
+ textWidth
+ (2 * _padding);
const auto left = _rect.x() + (_rect.width() - fullWidth) / 2;
_wrapped = QRect(left, _rect.y(), fullWidth, _rect.height());
p.drawRoundedRect(_wrapped, _radius, _radius);
p.setPen(_fg);
p.setFont(_font);
p.drawText(_wrapped.marginsRemoved(
{ _padding + (_emoji ? _emojiSize : 0), 0, _padding, 0 }),
text,
style::al_center);
}
[[nodiscard]] Data::ReactionId HeartReactionId() {
return { QString() + QChar(10084) };
}
@ -804,13 +1080,21 @@ auto Reactions::chosen() const -> rpl::producer<Chosen> {
auto Reactions::makeSuggestedReactionWidget(
const Data::SuggestedReaction &reaction)
-> std::unique_ptr<SuggestedReactionView> {
-> std::unique_ptr<StoryAreaView> {
return std::make_unique<ReactionView>(
_controller->wrap(),
&_controller->uiShow()->session(),
reaction);
}
auto Reactions::makeWeatherAreaWidget(const Data::WeatherArea &data)
-> std::unique_ptr<StoryAreaView> {
return std::make_unique<WeatherView>(
_controller->wrap(),
&_controller->uiShow()->session(),
data);
}
void Reactions::setReplyFieldState(
rpl::producer<bool> focused,
rpl::producer<bool> hasSendText) {

View file

@ -16,6 +16,7 @@ struct ReactionId;
class Session;
class Story;
struct SuggestedReaction;
struct WeatherArea;
} // namespace Data
namespace HistoryView::Reactions {
@ -41,13 +42,15 @@ enum class ReactionsMode {
Reaction,
};
class SuggestedReactionView {
class StoryAreaView {
public:
virtual ~SuggestedReactionView() = default;
virtual ~StoryAreaView() = default;
virtual void setAreaGeometry(QRect geometry) = 0;
virtual void updateCount(int count) = 0;
virtual void setAreaGeometry(QRect geometry, float64 radius) = 0;
virtual void updateReactionsCount(int count) = 0;
virtual void playEffect() = 0;
virtual void toggleMode() = 0;
virtual bool contains(QPoint point) = 0;
};
class Reactions final {
@ -79,7 +82,9 @@ public:
[[nodiscard]] auto makeSuggestedReactionWidget(
const Data::SuggestedReaction &reaction)
-> std::unique_ptr<SuggestedReactionView>;
-> std::unique_ptr<StoryAreaView>;
[[nodiscard]] auto makeWeatherAreaWidget(const Data::WeatherArea &data)
-> std::unique_ptr<StoryAreaView>;
void setReplyFieldState(
rpl::producer<bool> focused,

View file

@ -22,4 +22,12 @@ std::optional<QColor> MaybeColorFromSerialized(quint32 serialized) {
: std::make_optional(ColorFromSerialized(serialized));
}
QColor Color32FromSerialized(quint32 serialized) {
return QColor(
int((serialized >> 24) & 0xFFU),
int((serialized >> 16) & 0xFFU),
int((serialized >> 8) & 0xFFU),
int(serialized & 0xFFU));
}
} // namespace Ui

View file

@ -12,5 +12,6 @@ namespace Ui {
[[nodiscard]] QColor ColorFromSerialized(quint32 serialized);
[[nodiscard]] std::optional<QColor> MaybeColorFromSerialized(
quint32 serialized);
[[nodiscard]] QColor Color32FromSerialized(quint32 serialized);
} // namespace Ui