From d76c80bf0e3099b0d00c2725beeceb228b01f969 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 30 May 2023 21:12:15 +0400 Subject: [PATCH] Show recent viewers in self stories. --- Telegram/CMakeLists.txt | 2 + Telegram/Resources/langs/lang.strings | 3 + Telegram/SourceFiles/core/mime_type.cpp | 11 + Telegram/SourceFiles/core/mime_type.h | 1 + Telegram/SourceFiles/data/data_stories.cpp | 50 ++++- Telegram/SourceFiles/data/data_stories.h | 7 + .../view/history_view_replies_section.cpp | 13 +- .../view/history_view_scheduled_section.cpp | 16 +- .../stories/media_stories_controller.cpp | 20 +- .../media/stories/media_stories_controller.h | 3 + .../stories/media_stories_recent_views.cpp | 188 ++++++++++++++++++ .../stories/media_stories_recent_views.h | 53 +++++ .../media/stories/media_stories_reply.cpp | 11 +- .../media/stories/media_stories_reply.h | 6 - .../SourceFiles/media/view/media_view.style | 8 + 15 files changed, 342 insertions(+), 50 deletions(-) create mode 100644 Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp create mode 100644 Telegram/SourceFiles/media/stories/media_stories_recent_views.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index a32950631..c02353676 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -973,6 +973,8 @@ PRIVATE media/stories/media_stories_delegate.h media/stories/media_stories_header.cpp media/stories/media_stories_header.h + media/stories/media_stories_recent_views.cpp + media/stories/media_stories_recent_views.h media/stories/media_stories_reply.cpp media/stories/media_stories_reply.h media/stories/media_stories_sibling.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 460d20418..892983de5 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3793,6 +3793,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_stories_row_count#other" = "{count} Stories"; "lng_stories_row_unread_and_one" = "{accumulated}, {user}"; "lng_stories_row_unread_and_last" = "{accumulated} and {user}"; +"lng_stories_views#one" = "{count} view"; +"lng_stories_views#other" = "{count} views"; +"lng_stories_no_views" = "No views"; // Wnd specific diff --git a/Telegram/SourceFiles/core/mime_type.cpp b/Telegram/SourceFiles/core/mime_type.cpp index e75a5fc94..8ba1b6494 100644 --- a/Telegram/SourceFiles/core/mime_type.cpp +++ b/Telegram/SourceFiles/core/mime_type.cpp @@ -229,4 +229,15 @@ QList ReadMimeUrls(not_null data) { : QList(); } +bool CanSendFiles(not_null data) { + if (data->hasImage()) { + return true; + } else if (const auto urls = ReadMimeUrls(data); !urls.empty()) { + if (ranges::all_of(urls, &QUrl::isLocalFile)) { + return true; + } + } + return false; +} + } // namespace Core diff --git a/Telegram/SourceFiles/core/mime_type.h b/Telegram/SourceFiles/core/mime_type.h index d5aaa9eb0..3271adafe 100644 --- a/Telegram/SourceFiles/core/mime_type.h +++ b/Telegram/SourceFiles/core/mime_type.h @@ -67,5 +67,6 @@ struct MimeImageData { [[nodiscard]] MimeImageData ReadMimeImage(not_null data); [[nodiscard]] QString ReadMimeText(not_null data); [[nodiscard]] QList ReadMimeUrls(not_null data); +[[nodiscard]] bool CanSendFiles(not_null data); } // namespace Core diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index dd6d80fbb..39c5ae437 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -36,10 +36,10 @@ constexpr auto kMarkAsReadDelay = 3 * crl::time(1000); using UpdateFlag = StoryUpdate::Flag; std::optional ParseMedia( - not_null owner, - const MTPMessageMedia &media) { + not_null owner, + const MTPMessageMedia &media) { return media.match([&](const MTPDmessageMediaPhoto &data) - -> std::optional { + -> std::optional { if (const auto photo = data.vphoto()) { const auto result = owner->processPhoto(*photo); if (!result->isNull()) { @@ -48,7 +48,7 @@ std::optional ParseMedia( } return {}; }, [&](const MTPDmessageMediaDocument &data) - -> std::optional { + -> std::optional { if (const auto document = data.vdocument()) { const auto result = owner->processDocument(*document); if (!result->isNull() @@ -71,10 +71,10 @@ Story::Story( not_null peer, StoryMedia media, TimeId date) -: _id(id) -, _peer(peer) -, _media(std::move(media)) -, _date(date) { + : _id(id) + , _peer(peer) + , _media(std::move(media)) + , _date(date) { } Session &Story::owner() const { @@ -170,6 +170,21 @@ const TextWithEntities &Story::caption() const { return _caption; } +void Story::setViewsData( + std::vector> recent, + int total) { + _recentViewers = std::move(recent); + _views = total; +} + +const std::vector> &Story::recentViewers() const { + return _recentViewers; +} + +int Story::views() const { + return _views; +} + bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) { const auto pinned = data.is_pinned(); auto caption = TextWithEntities{ @@ -178,15 +193,32 @@ bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) { &owner().session(), data.ventities().value_or_empty()), }; + auto views = 0; + auto recent = std::vector>(); + if (const auto info = data.vviews()) { + views = info->data().vviews_count().v; + if (const auto list = info->data().vrecent_viewers()) { + recent.reserve(list->v.size()); + auto &owner = _peer->owner(); + for (const auto &id : list->v) { + recent.push_back(owner.peer(peerFromUser(id))); + } + } + } + const auto changed = (_media != media) || (_pinned != pinned) - || (_caption != caption); + || (_caption != caption) + || (_views != views) + || (_recentViewers != recent); if (!changed) { return false; } _media = std::move(media); _pinned = pinned; _caption = std::move(caption); + _views = views; + _recentViewers = std::move(recent); return true; } diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index 7a4546a0b..d8d7dcee3 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -57,6 +57,11 @@ public: void setCaption(TextWithEntities &&caption); [[nodiscard]] const TextWithEntities &caption() const; + void setViewsData(std::vector> recent, int total); + [[nodiscard]] auto recentViewers() const + -> const std::vector> &; + [[nodiscard]] int views() const; + bool applyChanges(StoryMedia media, const MTPDstoryItem &data); private: @@ -64,6 +69,8 @@ private: const not_null _peer; StoryMedia _media; TextWithEntities _caption; + std::vector> _recentViewers; + int _views = 0; const TimeId _date = 0; bool _pinned = false; diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index aeacfd251..0c1b268af 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -97,17 +97,6 @@ namespace { constexpr auto kRefreshSlowmodeLabelTimeout = crl::time(200); -bool CanSendFiles(not_null data) { - if (data->hasImage()) { - return true; - } else if (const auto urls = Core::ReadMimeUrls(data); !urls.empty()) { - if (ranges::all_of(urls, &QUrl::isLocalFile)) { - return true; - } - } - return false; -} - rpl::producer RootViewContent( not_null history, MsgId rootId, @@ -819,7 +808,7 @@ void RepliesWidget::setupComposeControls() { not_null data, Ui::InputField::MimeAction action) { if (action == Ui::InputField::MimeAction::Check) { - return CanSendFiles(data); + return Core::CanSendFiles(data); } else if (action == Ui::InputField::MimeAction::Insert) { return confirmSendingFiles( data, diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index 66255b466..77922c6c1 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -65,20 +65,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include namespace HistoryView { -namespace { - -bool CanSendFiles(not_null data) { - if (data->hasImage()) { - return true; - } else if (const auto urls = Core::ReadMimeUrls(data); !urls.empty()) { - if (ranges::all_of(urls, &QUrl::isLocalFile)) { - return true; - } - } - return false; -} - -} // namespace object_ptr ScheduledMemento::createWidget( QWidget *parent, @@ -308,7 +294,7 @@ void ScheduledWidget::setupComposeControls() { not_null data, Ui::InputField::MimeAction action) { if (action == Ui::InputField::MimeAction::Check) { - return CanSendFiles(data); + return Core::CanSendFiles(data); } else if (action == Ui::InputField::MimeAction::Insert) { return confirmSendingFiles( data, diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp index 6921f7b80..6d11fd247 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/stories/media_stories_header.h" #include "media/stories/media_stories_sibling.h" #include "media/stories/media_stories_slider.h" +#include "media/stories/media_stories_recent_views.h" #include "media/stories/media_stories_reply.h" #include "media/stories/media_stories_view.h" #include "media/audio/media_audio.h" @@ -123,7 +124,8 @@ Controller::Controller(not_null delegate) , _wrap(_delegate->storiesWrap()) , _header(std::make_unique
(this)) , _slider(std::make_unique(this)) -, _replyArea(std::make_unique(this)) { +, _replyArea(std::make_unique(this)) +, _recentViews(std::make_unique(this)) { initLayout(); _replyArea->activeValue( @@ -249,6 +251,9 @@ void Controller::initLayout() { + layout.content.height() + fieldMinHeight - st::storiesFieldMargin.bottom())); + layout.views = QRect( + layout.controlsBottomPosition - QPoint(0, fieldMinHeight), + QSize(layout.controlsWidth, fieldMinHeight)); layout.autocompleteRect = QRect( layout.controlsBottomPosition.x(), 0, @@ -422,9 +427,18 @@ void Controller::show( _captionText = story->caption(); _captionFullView = nullptr; + if (_replyFocused) { + unfocusReply(); + } + _header->show({ .user = list.user, .date = story->date() }); _slider->show({ .index = _index, .total = list.total }); _replyArea->show({ .user = list.user, .id = id }); + _recentViews->show({ + .list = story->recentViewers(), + .total = story->views(), + .valid = list.user->isSelf(), + }); const auto session = &list.user->session(); if (_session != session) { @@ -450,11 +464,7 @@ void Controller::show( } stories.loadAround(storyId); - if (_replyFocused) { - unfocusReply(); - } updatePlayingAllowed(); - list.user->updateFull(); } diff --git a/Telegram/SourceFiles/media/stories/media_stories_controller.h b/Telegram/SourceFiles/media/stories/media_stories_controller.h index 47adde27e..eef7cc1a3 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_controller.h +++ b/Telegram/SourceFiles/media/stories/media_stories_controller.h @@ -41,6 +41,7 @@ namespace Media::Stories { class Header; class Slider; class ReplyArea; +class RecentViews; class Sibling; class Delegate; struct SiblingView; @@ -68,6 +69,7 @@ struct Layout { QRect slider; int controlsWidth = 0; QPoint controlsBottomPosition; + QRect views; QRect autocompleteRect; HeaderLayout headerLayout = HeaderLayout::Normal; SiblingLayout siblingLeft; @@ -148,6 +150,7 @@ private: const std::unique_ptr
_header; const std::unique_ptr _slider; const std::unique_ptr _replyArea; + const std::unique_ptr _recentViews; std::unique_ptr _photoPlayback; std::unique_ptr _captionFullView; diff --git a/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp new file mode 100644 index 000000000..9148e6b6f --- /dev/null +++ b/Telegram/SourceFiles/media/stories/media_stories_recent_views.cpp @@ -0,0 +1,188 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "media/stories/media_stories_recent_views.h" + +#include "data/data_peer.h" +#include "main/main_session.h" +#include "media/stories/media_stories_controller.h" +#include "lang/lang_keys.h" +#include "ui/chat/group_call_userpics.h" +#include "ui/painter.h" +#include "ui/rp_widget.h" +#include "ui/userpic_view.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_media_view.h" + +namespace Media::Stories { +namespace { + +[[nodiscard]] rpl::producer> ContentByUsers( + const std::vector> &list) { + struct Userpic { + not_null peer; + mutable Ui::PeerUserpicView view; + mutable InMemoryKey uniqueKey; + }; + + struct State { + std::vector userpics; + std::vector current; + base::has_weak_ptr guard; + bool someUserpicsNotLoaded = false; + bool scheduled = false; + }; + + static const auto size = st::storiesRecentViewsUserpics.size; + + static const auto GenerateUserpic = [](Userpic &userpic) { + auto result = userpic.peer->generateUserpicImage( + userpic.view, + size * style::DevicePixelRatio()); + result.setDevicePixelRatio(style::DevicePixelRatio()); + return result; + }; + + static const auto RegenerateUserpics = [](not_null state) { + Expects(state->userpics.size() == state->current.size()); + + state->someUserpicsNotLoaded = false; + const auto count = int(state->userpics.size()); + for (auto i = 0; i != count; ++i) { + auto &userpic = state->userpics[i]; + auto &participant = state->current[i]; + const auto peer = userpic.peer; + const auto key = peer->userpicUniqueKey(userpic.view); + if (peer->hasUserpic() && peer->useEmptyUserpic(userpic.view)) { + state->someUserpicsNotLoaded = true; + } + if (userpic.uniqueKey == key) { + continue; + } + participant.userpicKey = userpic.uniqueKey = key; + participant.userpic = GenerateUserpic(userpic); + } + }; + + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + + const auto state = lifetime.make_state(); + const auto pushNext = [=] { + RegenerateUserpics(state); + consumer.put_next_copy(state->current); + }; + + for (const auto &peer : list) { + state->userpics.push_back(Userpic{ + .peer = peer, + }); + state->current.push_back(Ui::GroupCallUser{ + .id = uint64(peer->id.value), + }); + peer->loadUserpic(); + } + pushNext(); + + if (!list.empty()) { + list.front()->session().downloaderTaskFinished( + ) | rpl::filter([=] { + return state->someUserpicsNotLoaded && !state->scheduled; + }) | rpl::start_with_next([=] { + for (const auto &userpic : state->userpics) { + if (userpic.peer->userpicUniqueKey(userpic.view) + != userpic.uniqueKey) { + state->scheduled = true; + crl::on_main(&state->guard, [=] { + state->scheduled = false; + pushNext(); + }); + return; + } + } + }, lifetime); + } + return lifetime; + }; +} + +} // namespace + +RecentViews::RecentViews(not_null controller) +: _controller(controller) { +} + +RecentViews::~RecentViews() = default; + +void RecentViews::show(RecentViewsData data) { + if (_data == data) { + return; + } + const auto totalChanged = _text.isEmpty() || (_data.total != data.total); + const auto usersChanged = !_userpics || (_data.list != data.list); + _data = data; + if (!_data.valid) { + _text = {}; + _userpics = nullptr; + _widget = nullptr; + return; + } + if (!_widget) { + const auto parent = _controller->wrap(); + auto widget = std::make_unique(parent); + const auto raw = widget.get(); + raw->show(); + + _controller->layoutValue( + ) | rpl::start_with_next([=](const Layout &layout) { + raw->setGeometry(layout.views); + }, raw->lifetime()); + + raw->paintRequest( + ) | rpl::start_with_next([=](QRect clip) { + auto p = Painter(raw); + const auto skip = st::storiesRecentViewsSkip; + const auto full = _userpicsWidth + skip + _text.maxWidth(); + const auto use = std::min(full, raw->width()); + const auto ux = (raw->width() - use) / 2; + const auto height = st::storiesRecentViewsUserpics.size; + const auto uy = (raw->height() - height) / 2; + const auto tx = ux + _userpicsWidth + skip; + const auto ty = (raw->height() - st::normalFont->height) / 2; + _userpics->paint(p, ux, uy, height); + p.setPen(st::storiesComposeWhiteText); + _text.drawElided(p, tx, ty, use - _userpicsWidth - skip); + }, raw->lifetime()); + + _widget = std::move(widget); + } + if (totalChanged) { + _text.setText(st::defaultTextStyle, data.total + ? tr::lng_stories_views(tr::now, lt_count, data.total) + : tr::lng_stories_no_views(tr::now)); + } + if (!_userpics) { + _userpics = std::make_unique( + st::storiesRecentViewsUserpics, + rpl::single(true), + [=] { _widget->update(); }); + + _userpics->widthValue() | rpl::start_with_next([=](int width) { + _userpicsWidth = width; + }, _widget->lifetime()); + } + if (usersChanged) { + _userpicsLifetime = ContentByUsers( + data.list + ) | rpl::start_with_next([=]( + const std::vector &list) { + _userpics->update(list, true); + }); + } +} + +} // namespace Media::Stories diff --git a/Telegram/SourceFiles/media/stories/media_stories_recent_views.h b/Telegram/SourceFiles/media/stories/media_stories_recent_views.h new file mode 100644 index 000000000..64a6d4e82 --- /dev/null +++ b/Telegram/SourceFiles/media/stories/media_stories_recent_views.h @@ -0,0 +1,53 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "ui/text/text.h" + +namespace Ui { +class RpWidget; +class GroupCallUserpics; +} // namespace Ui + +namespace Media::Stories { + +class Controller; + +struct RecentViewsData { + std::vector> list; + int total = 0; + bool valid = false; + + friend inline auto operator<=>( + const RecentViewsData &, + const RecentViewsData &) = default; + friend inline bool operator==( + const RecentViewsData &, + const RecentViewsData &) = default; +}; + +class RecentViews final { +public: + explicit RecentViews(not_null controller); + ~RecentViews(); + + void show(RecentViewsData data); + +private: + const not_null _controller; + + std::unique_ptr _widget; + std::unique_ptr _userpics; + Ui::Text::String _text; + RecentViewsData _data; + rpl::lifetime _userpicsLifetime; + int _userpicsWidth = 0; + +}; + +} // namespace Media::Stories \ No newline at end of file diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index 92abe1133..cbff23d73 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -524,12 +524,12 @@ void ReplyArea::initActions() { not_null data, Ui::InputField::MimeAction action) { if (action == Ui::InputField::MimeAction::Check) { - return false;// checkSendingFiles(data); + return Core::CanSendFiles(data); } else if (action == Ui::InputField::MimeAction::Insert) { - return false;/* confirmSendingFiles( + return confirmSendingFiles( data, std::nullopt, - Core::ReadMimeText(data));*/ + Core::ReadMimeText(data)); } Unexpected("action in MimeData hook."); }); @@ -562,6 +562,11 @@ void ReplyArea::show(ReplyAreaData data) { .history = history, }); _controls->clear(); + if (!user || user->isSelf()) { + _controls->hide(); + } else { + _controls->show(); + } } Main::Session &ReplyArea::session() const { diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.h b/Telegram/SourceFiles/media/stories/media_stories_reply.h index d0cef32c3..d50496d5d 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.h +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.h @@ -66,18 +66,12 @@ private: [[nodiscard]] Main::Session &session() const; [[nodiscard]] not_null history() const; - bool confirmSendingFiles(const QStringList &files); - bool confirmSendingFiles(not_null data); - void uploadFile(const QByteArray &fileContent, SendMediaType type); bool confirmSendingFiles( QImage &&image, QByteArray &&content, std::optional overrideSendImagesAsPhotos = std::nullopt, const QString &insertTextOnCancel = QString()); - bool confirmSendingFiles( - const QStringList &files, - const QString &insertTextOnCancel); bool confirmSendingFiles( Ui::PreparedList &&list, const QString &insertTextOnCancel = QString()); diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index bded9e12e..2a3c3f7b2 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -729,3 +729,11 @@ storiesComposeControls: ComposeControls(defaultComposeControls) { } premium: storiesComposePremium; } +storiesRecentViewsUserpics: GroupCallUserpics { + size: 24px; + shift: 9px; + stroke: 4px; + align: align(left); +} +storiesRecentViewsSkip: 8px; +