Show recent viewers in self stories.

This commit is contained in:
John Preston 2023-05-30 21:12:15 +04:00
parent e90642f3a0
commit d76c80bf0e
15 changed files with 342 additions and 50 deletions

View File

@ -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

View File

@ -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

View File

@ -229,4 +229,15 @@ QList<QUrl> ReadMimeUrls(not_null<const QMimeData*> data) {
: QList<QUrl>();
}
bool CanSendFiles(not_null<const QMimeData*> 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

View File

@ -67,5 +67,6 @@ struct MimeImageData {
[[nodiscard]] MimeImageData ReadMimeImage(not_null<const QMimeData*> data);
[[nodiscard]] QString ReadMimeText(not_null<const QMimeData*> data);
[[nodiscard]] QList<QUrl> ReadMimeUrls(not_null<const QMimeData*> data);
[[nodiscard]] bool CanSendFiles(not_null<const QMimeData*> data);
} // namespace Core

View File

@ -36,10 +36,10 @@ constexpr auto kMarkAsReadDelay = 3 * crl::time(1000);
using UpdateFlag = StoryUpdate::Flag;
std::optional<StoryMedia> ParseMedia(
not_null<Session*> owner,
const MTPMessageMedia &media) {
not_null<Session*> owner,
const MTPMessageMedia &media) {
return media.match([&](const MTPDmessageMediaPhoto &data)
-> std::optional<StoryMedia> {
-> std::optional<StoryMedia> {
if (const auto photo = data.vphoto()) {
const auto result = owner->processPhoto(*photo);
if (!result->isNull()) {
@ -48,7 +48,7 @@ std::optional<StoryMedia> ParseMedia(
}
return {};
}, [&](const MTPDmessageMediaDocument &data)
-> std::optional<StoryMedia> {
-> std::optional<StoryMedia> {
if (const auto document = data.vdocument()) {
const auto result = owner->processDocument(*document);
if (!result->isNull()
@ -71,10 +71,10 @@ Story::Story(
not_null<PeerData*> 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<not_null<PeerData*>> recent,
int total) {
_recentViewers = std::move(recent);
_views = total;
}
const std::vector<not_null<PeerData*>> &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<not_null<PeerData*>>();
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;
}

View File

@ -57,6 +57,11 @@ public:
void setCaption(TextWithEntities &&caption);
[[nodiscard]] const TextWithEntities &caption() const;
void setViewsData(std::vector<not_null<PeerData*>> recent, int total);
[[nodiscard]] auto recentViewers() const
-> const std::vector<not_null<PeerData*>> &;
[[nodiscard]] int views() const;
bool applyChanges(StoryMedia media, const MTPDstoryItem &data);
private:
@ -64,6 +69,8 @@ private:
const not_null<PeerData*> _peer;
StoryMedia _media;
TextWithEntities _caption;
std::vector<not_null<PeerData*>> _recentViewers;
int _views = 0;
const TimeId _date = 0;
bool _pinned = false;

View File

@ -97,17 +97,6 @@ namespace {
constexpr auto kRefreshSlowmodeLabelTimeout = crl::time(200);
bool CanSendFiles(not_null<const QMimeData*> 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<Ui::MessageBarContent> RootViewContent(
not_null<History*> history,
MsgId rootId,
@ -819,7 +808,7 @@ void RepliesWidget::setupComposeControls() {
not_null<const QMimeData*> 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,

View File

@ -65,20 +65,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QtCore/QMimeData>
namespace HistoryView {
namespace {
bool CanSendFiles(not_null<const QMimeData*> 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<Window::SectionWidget> ScheduledMemento::createWidget(
QWidget *parent,
@ -308,7 +294,7 @@ void ScheduledWidget::setupComposeControls() {
not_null<const QMimeData*> 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,

View File

@ -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*> delegate)
, _wrap(_delegate->storiesWrap())
, _header(std::make_unique<Header>(this))
, _slider(std::make_unique<Slider>(this))
, _replyArea(std::make_unique<ReplyArea>(this)) {
, _replyArea(std::make_unique<ReplyArea>(this))
, _recentViews(std::make_unique<RecentViews>(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();
}

View File

@ -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> _header;
const std::unique_ptr<Slider> _slider;
const std::unique_ptr<ReplyArea> _replyArea;
const std::unique_ptr<RecentViews> _recentViews;
std::unique_ptr<PhotoPlayback> _photoPlayback;
std::unique_ptr<CaptionFullView> _captionFullView;

View File

@ -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<std::vector<Ui::GroupCallUser>> ContentByUsers(
const std::vector<not_null<PeerData*>> &list) {
struct Userpic {
not_null<PeerData*> peer;
mutable Ui::PeerUserpicView view;
mutable InMemoryKey uniqueKey;
};
struct State {
std::vector<Userpic> userpics;
std::vector<Ui::GroupCallUser> 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*> 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<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(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<Ui::RpWidget>(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<Ui::GroupCallUserpics>(
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<Ui::GroupCallUser> &list) {
_userpics->update(list, true);
});
}
}
} // namespace Media::Stories

View File

@ -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<not_null<PeerData*>> 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*> controller);
~RecentViews();
void show(RecentViewsData data);
private:
const not_null<Controller*> _controller;
std::unique_ptr<Ui::RpWidget> _widget;
std::unique_ptr<Ui::GroupCallUserpics> _userpics;
Ui::Text::String _text;
RecentViewsData _data;
rpl::lifetime _userpicsLifetime;
int _userpicsWidth = 0;
};
} // namespace Media::Stories

View File

@ -524,12 +524,12 @@ void ReplyArea::initActions() {
not_null<const QMimeData*> 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 {

View File

@ -66,18 +66,12 @@ private:
[[nodiscard]] Main::Session &session() const;
[[nodiscard]] not_null<History*> history() const;
bool confirmSendingFiles(const QStringList &files);
bool confirmSendingFiles(not_null<const QMimeData*> data);
void uploadFile(const QByteArray &fileContent, SendMediaType type);
bool confirmSendingFiles(
QImage &&image,
QByteArray &&content,
std::optional<bool> overrideSendImagesAsPhotos = std::nullopt,
const QString &insertTextOnCancel = QString());
bool confirmSendingFiles(
const QStringList &files,
const QString &insertTextOnCancel);
bool confirmSendingFiles(
Ui::PreparedList &&list,
const QString &insertTextOnCancel = QString());

View File

@ -729,3 +729,11 @@ storiesComposeControls: ComposeControls(defaultComposeControls) {
}
premium: storiesComposePremium;
}
storiesRecentViewsUserpics: GroupCallUserpics {
size: 24px;
shift: 9px;
stroke: 4px;
align: align(left);
}
storiesRecentViewsSkip: 8px;