Show reposts / forwards in story viewers.

This commit is contained in:
John Preston 2023-12-14 22:59:50 +00:00
parent 8e92778b62
commit d87a0a2d25
15 changed files with 188 additions and 39 deletions

View File

@ -1836,6 +1836,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_sponsored_message_title" = "Sponsored";
"lng_recommended_message_title" = "Recommended";
"lng_edited" = "edited";
"lng_commented" = "commented";
"lng_edited_date" = "Edited: {date}";
"lng_sent_date" = "Sent: {date}";
"lng_views_tooltip#one" = "Views: {count}";

View File

@ -399,7 +399,7 @@ void PublicForwards::request(
recentList.push_back({ .messageId = { peerId, msgId } });
}
}, [&](const MTPDpublicForwardStory &data) {
const auto story = owner.stories().applyFromWebpage(
const auto story = owner.stories().applySingle(
peerFromMTP(data.vpeer()),
data.vstory());
if (story) {

View File

@ -3416,7 +3416,7 @@ void Session::webpageApplyFields(
data.vid().v,
};
if (const auto embed = data.vstory()) {
story = stories().applyFromWebpage(
story = stories().applySingle(
peerFromMTP(data.vpeer()),
*embed);
} else if (const auto maybe = stories().lookup(storyId)) {

View File

@ -220,7 +220,7 @@ void Stories::apply(not_null<PeerData*> peer, const MTPPeerStories *data) {
}
}
Story *Stories::applyFromWebpage(PeerId peerId, const MTPstoryItem &story) {
Story *Stories::applySingle(PeerId peerId, const MTPstoryItem &story) {
const auto idDates = parseAndApply(
_owner->peer(peerId),
story,
@ -1374,6 +1374,8 @@ void Stories::sendViewsSliceRequest() {
auto slice = StoryViews{
.nextOffset = data.vnext_offset().value_or_empty(),
.reactions = data.vreactions_count().v,
.forwards = data.vforwards_count().v,
.views = data.vviews_count().v,
.total = data.vcount().v,
};
_owner->processUsers(data.vusers());
@ -1387,8 +1389,27 @@ void Stories::sendViewsSliceRequest() {
: Data::ReactionId()),
.date = data.vdate().v,
});
}, [](const auto &) {
}, [&](const MTPDstoryViewPublicRepost &data) {
const auto story = applySingle(
peerFromMTP(data.vpeer_id()),
data.vstory());
if (story) {
slice.list.push_back({
.peer = story->peer(),
.repostId = story->id(),
});
}
}, [&](const MTPDstoryViewPublicForward &data) {
const auto item = _owner->addNewMessage(
data.vmessage(),
{},
NewMessageType::Existing);
if (item) {
slice.list.push_back({
.peer = item->history()->peer,
.forwardId = item->id,
});
}
});
}
const auto fullId = FullStoryId{

View File

@ -149,7 +149,7 @@ public:
void apply(const MTPDupdateReadStories &data);
void apply(const MTPStoriesStealthMode &stealthMode);
void apply(not_null<PeerData*> peer, const MTPPeerStories *data);
Story *applyFromWebpage(PeerId peerId, const MTPstoryItem &story);
Story *applySingle(PeerId peerId, const MTPstoryItem &story);
void loadAround(FullStoryId id, StoriesContext context);
const StoriesSource *source(PeerId id) const;

View File

@ -157,6 +157,13 @@ using UpdateFlag = StoryUpdate::Flag;
return {};
}
[[nodiscard]] bool RepostModified(const MTPDstoryItem &data) {
if (const auto forwarded = data.vfwd_from()) {
return forwarded->data().is_modified();
}
return false;
}
} // namespace
class StoryPreload::LoadTask final : private Storage::DownloadMtprotoTask {
@ -272,7 +279,8 @@ Story::Story(
, _repostSourceName(RepostSourceName(data))
, _repostSourceId(RepostSourceId(data))
, _date(data.vdate().v)
, _expires(data.vexpire_date().v) {
, _expires(data.vexpire_date().v)
, _repostModified(RepostModified(data)) {
applyFields(std::move(media), data, now, true);
}
@ -505,10 +513,18 @@ const StoryViews &Story::viewsList() const {
return _views;
}
int Story::views() const {
int Story::interactions() const {
return _views.total;
}
int Story::views() const {
return _views.views;
}
int Story::forwards() const {
return _views.forwards;
}
int Story::reactions() const {
return _views.reactions;
}
@ -517,8 +533,12 @@ void Story::applyViewsSlice(
const QString &offset,
const StoryViews &slice) {
const auto changed = (_views.reactions != slice.reactions)
|| (_views.views != slice.views)
|| (_views.forwards != slice.forwards)
|| (_views.total != slice.total);
_views.reactions = slice.reactions;
_views.forwards = slice.forwards;
_views.views = slice.views;
_views.total = slice.total;
_views.known = true;
if (offset.isEmpty()) {
@ -536,6 +556,15 @@ void Story::applyViewsSlice(
_views.list,
Data::ReactionId(),
&StoryView::reaction);
_views.forwards = _views.total
- ranges::count(
_views.list,
0,
[](const StoryView &view) {
return view.repostId
? view.repostId
: view.forwardId.bare;
});
}
}
const auto known = int(_views.list.size());
@ -582,6 +611,7 @@ Story::ViewsCounts Story::parseViewsCounts(
const Data::ReactionId &mine) {
auto result = ViewsCounts{
.views = data.vviews_count().v,
.forwards = data.vforwards_count().value_or_empty(),
.reactions = data.vreactions_count().value_or_empty(),
};
if (const auto list = data.vrecent_viewers()) {
@ -660,6 +690,7 @@ void Story::applyFields(
viewsKnown = true;
} else {
counts.views = _views.total;
counts.forwards = _views.forwards;
counts.reactions = _views.reactions;
counts.viewers = _recentViewers;
for (const auto &suggested : _suggestedReactions) {
@ -744,15 +775,22 @@ void Story::applyFields(
}
void Story::updateViewsCounts(ViewsCounts &&counts, bool known, bool initial) {
const auto viewsChanged = (_views.total != counts.views)
const auto total = _views.total
? _views.total
: (counts.views + counts.forwards);
const auto viewsChanged = (_views.total != total)
|| (_views.forwards != counts.forwards)
|| (_views.reactions != counts.reactions)
|| (_recentViewers != counts.viewers);
if (_views.reactions != counts.reactions
|| _views.total != counts.views
|| _views.forwards != counts.forwards
|| _views.total != total
|| _views.known != known) {
_views = StoryViews{
.reactions = counts.reactions,
.total = counts.views,
.forwards = counts.forwards,
.views = counts.views,
.total = total,
.known = known,
};
}
@ -789,6 +827,10 @@ bool Story::repost() const {
return _repostSourcePeer || !_repostSourceName.isEmpty();
}
bool Story::repostModified() const {
return _repostModified;
}
PeerData *Story::repostSourcePeer() const {
return _repostSourcePeer;
}

View File

@ -61,6 +61,8 @@ struct StoryMedia {
struct StoryView {
not_null<PeerData*> peer;
Data::ReactionId reaction;
StoryId repostId = 0;
MsgId forwardId = 0;
TimeId date = 0;
friend inline bool operator==(StoryView, StoryView) = default;
@ -70,6 +72,8 @@ struct StoryViews {
std::vector<StoryView> list;
QString nextOffset;
int reactions = 0;
int forwards = 0;
int views = 0;
int total = 0;
bool known = false;
};
@ -175,7 +179,9 @@ public:
[[nodiscard]] auto recentViewers() const
-> const std::vector<not_null<PeerData*>> &;
[[nodiscard]] const StoryViews &viewsList() const;
[[nodiscard]] int interactions() const;
[[nodiscard]] int views() const;
[[nodiscard]] int forwards() const;
[[nodiscard]] int reactions() const;
void applyViewsSlice(const QString &offset, const StoryViews &slice);
@ -191,6 +197,7 @@ public:
[[nodiscard]] TimeId lastUpdateTime() const;
[[nodiscard]] bool repost() const;
[[nodiscard]] bool repostModified() const;
[[nodiscard]] PeerData *repostSourcePeer() const;
[[nodiscard]] QString repostSourceName() const;
[[nodiscard]] StoryId repostSourceId() const;
@ -198,6 +205,7 @@ public:
private:
struct ViewsCounts {
int views = 0;
int forwards = 0;
int reactions = 0;
base::flat_map<Data::ReactionId, int> reactionsCounts;
std::vector<not_null<PeerData*>> viewers;
@ -236,6 +244,7 @@ private:
bool _privacyCloseFriends : 1 = false;
bool _privacyContacts : 1 = false;
bool _privacySelectedContacts : 1 = false;
const bool _repostModified : 1 = false;
bool _noForwards : 1 = false;
bool _edited : 1 = false;

View File

@ -903,7 +903,9 @@ void Controller::show(
_recentViews->show({
.list = story->recentViewers(),
.reactions = story->reactions(),
.total = story->views(),
.forwards = story->forwards(),
.views = story->views(),
.total = story->interactions(),
.type = RecentViewsTypeFor(peer),
}, _reactions->likedValue());
if (const auto nowLikeButton = _recentViews->likeButton()) {
@ -999,7 +1001,9 @@ void Controller::subscribeToSession() {
_recentViews->show({
.list = update.story->recentViewers(),
.reactions = update.story->reactions(),
.total = update.story->views(),
.forwards = update.story->forwards(),
.views = update.story->views(),
.total = update.story->interactions(),
.type = RecentViewsTypeFor(update.story->peer()),
});
updateAreas(update.story);

View File

@ -256,7 +256,7 @@ struct MadePrivacyBadge {
QString::fromUtf8(" \xE2\x80\xA2 ") + tr::lng_edited(tr::now));
}
if (!data.repostFrom.isEmpty()) {
result.text = QString::fromUtf8("\xC2\xA0\xE2\x80\xA2 ")
result.text = QString::fromUtf8("\xE2\x80\xA2 ")
+ result.text;
}
return result;
@ -500,7 +500,8 @@ void Header::show(HeaderData data) {
_repost->resizeToNaturalWidth(repostAvailable);
}
_repost->move(dateLeft, dateTop);
_date->move(dateLeft + _repost->width(), dateTop);
const auto space = st::normalFont->spacew;
_date->move(dateLeft + _repost->width() + space, dateTop);
} else {
_date->move(dateLeft, dateTop);
}

View File

@ -11,7 +11,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/compose/compose_show.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_stories.h"
#include "history/history_item.h"
#include "main/main_session.h"
#include "media/stories/media_stories_controller.h"
#include "lang/lang_keys.h"
@ -121,6 +123,16 @@ constexpr auto kLoadViewsPages = 2;
};
}
[[nodiscard]] QString ComposeRepostStatus(
const QString &date,
not_null<Data::Story*> repost) {
return date + (repost->repostModified()
? (QString::fromUtf8(" \xE2\x80\xA2 ") + tr::lng_edited(tr::now))
: !repost->caption().empty()
? (QString::fromUtf8(" \xE2\x80\xA2 ") + tr::lng_commented(tr::now))
: QString());
}
} // namespace
RecentViewsType RecentViewsTypeFor(not_null<PeerData*> peer) {
@ -162,6 +174,8 @@ void RecentViews::show(
}
const auto countersChanged = _text.isEmpty()
|| (_data.total != data.total)
|| (_data.views != data.views)
|| (_data.forwards != data.forwards)
|| (_data.reactions != data.reactions);
const auto usersChanged = !_userpics || (_data.list != data.list);
_data = data;
@ -194,7 +208,7 @@ void RecentViews::show(
_viewsWrap = nullptr;
} else {
_viewsCounter = (_data.type == RecentViewsType::Channel)
? Lang::FormatCountDecimal(std::max(_data.total, 1))
? Lang::FormatCountDecimal(std::max(_data.views, 1))
: tr::lng_stories_cant_reply(tr::now);
_likesCounter = ((_data.type == RecentViewsType::Channel)
&& _data.reactions)
@ -392,8 +406,8 @@ void RecentViews::updatePartsGeometry() {
}
void RecentViews::updateText() {
const auto text = _data.total
? (tr::lng_stories_views(tr::now, lt_count, _data.total)
const auto text = _data.views
? (tr::lng_stories_views(tr::now, lt_count, _data.views)
+ (_data.reactions
? (u" "_q + QChar(10084) + QString::number(_data.reactions))
: QString()))
@ -473,7 +487,21 @@ void RecentViews::addMenuRow(Data::StoryView entry, const QDateTime &now) {
Expects(_menu != nullptr);
const auto peer = entry.peer;
const auto date = Api::FormatReadDate(entry.date, now);
const auto repost = entry.repostId
? peer->owner().stories().lookup({ peer->id, entry.repostId })
: base::make_unexpected(Data::NoStory::Deleted);
const auto forward = entry.forwardId
? peer->owner().message({ peer->id, entry.forwardId })
: nullptr;
const auto date = Api::FormatReadDate(
repost ? (*repost)->date() : forward ? forward->date() : entry.date,
now);
const auto type = forward
? Ui::WhoReactedType::Forwarded
: repost
? Ui::WhoReactedType::Reposted
: Ui::WhoReactedType::Viewed;
const auto status = repost ? ComposeRepostStatus(date, *repost) : date;
const auto show = _controller->uiShow();
const auto prepare = [&](Ui::PeerUserpicView &view) {
const auto size = st::storiesWhoViewed.photoSize;
@ -483,7 +511,8 @@ void RecentViews::addMenuRow(Data::StoryView entry, const QDateTime &now) {
userpic.setDevicePixelRatio(style::DevicePixelRatio());
return Ui::WhoReactedEntryData{
.text = peer->name(),
.date = date,
.date = status,
.type = type,
.customEntityData = Data::ReactionEntityData(entry.reaction),
.userpic = std::move(userpic),
.callback = [=] { show->show(PrepareShortInfoBox(peer)); },
@ -493,7 +522,8 @@ void RecentViews::addMenuRow(Data::StoryView entry, const QDateTime &now) {
const auto i = _menuEntries.end() - (_menuPlaceholderCount--);
auto data = prepare(i->view);
i->peer = peer;
i->date = date;
i->type = type;
i->status = status;
i->customEntityData = data.customEntityData;
i->callback = data.callback;
i->action->setData(std::move(data));
@ -512,7 +542,8 @@ void RecentViews::addMenuRow(Data::StoryView entry, const QDateTime &now) {
_menuEntries.push_back({
.action = raw,
.peer = peer,
.date = date,
.type = type,
.status = status,
.customEntityData = std::move(customEntityData),
.callback = std::move(callback),
.view = std::move(view),
@ -533,7 +564,7 @@ void RecentViews::addMenuRowPlaceholder(not_null<Main::Session*> session) {
_menu->menu(),
Data::ReactedMenuFactory(session),
_menu->menu()->st(),
Ui::WhoReactedEntryData{ .preloader = true });
Ui::WhoReactedEntryData{ .type = Ui::WhoReactedType::Preloader });
const auto raw = action.get();
_menu->addAction(std::move(action));
_menuEntries.push_back({ .action = raw });
@ -550,11 +581,15 @@ void RecentViews::rebuildMenuTail() {
const auto added = std::min(
_menuPlaceholderCount + kAddPerPage,
int(views.list.size() - elements));
const auto height = _menu->height();
for (auto i = elements, till = i + added; i != till; ++i) {
const auto &entry = views.list[i];
addMenuRow(entry, now);
}
_menuEntriesCount = _menuEntriesCount.current() + added;
if (const auto delta = _menu->height() - height) {
_menu->move(_menu->x(), _menu->y() - delta);
}
}
void RecentViews::subscribeToMenuUserpicsLoading(
@ -590,7 +625,8 @@ void RecentViews::subscribeToMenuUserpicsLoading(
userpic.setDevicePixelRatio(style::DevicePixelRatio());
entry.action->setData({
.text = peer->name(),
.date = entry.date,
.date = entry.status,
.type = entry.type,
.customEntityData = entry.customEntityData,
.userpic = std::move(userpic),
.callback = entry.callback,

View File

@ -23,6 +23,7 @@ class RpWidget;
class GroupCallUserpics;
class PopupMenu;
class WhoReactedEntryAction;
enum class WhoReactedType : uchar;
} // namespace Ui
namespace Main {
@ -43,6 +44,8 @@ enum class RecentViewsType {
struct RecentViewsData {
std::vector<not_null<PeerData*>> list;
int reactions = 0;
int forwards = 0;
int views = 0;
int total = 0;
RecentViewsType type = RecentViewsType::Other;
@ -72,7 +75,8 @@ private:
struct MenuEntry {
not_null<Ui::WhoReactedEntryAction*> action;
PeerData *peer = nullptr;
QString date;
Ui::WhoReactedType type = {};
QString status;
QString customEntityData;
Fn<void()> callback;
Ui::PeerUserpicView view;

View File

@ -5019,6 +5019,7 @@ void OverlayWidget::paintCaptionContent(
const auto inner = full.marginsRemoved(
_stories ? _stories->repostCaptionPadding() : QMargins());
if (_stories) {
p.setOpacity(1.);
if (_stories->repost()) {
_stories->drawRepostInfo(p, full.x(), full.y(), full.width());
}

View File

@ -791,6 +791,10 @@ whoReadDateChecks: icon{{ "menu/read_ticks_s", windowSubTextFg }};
whoReadDateChecksOver: icon{{ "menu/read_ticks_s", windowSubTextFgOver }};
whoLikedDateHeart: icon{{ "menu/read_react_s", windowSubTextFg }};
whoLikedDateHeartOver: icon{{ "menu/read_react_s", windowSubTextFgOver }};
whoRepostedDateHeart: icon{{ "mediaview/mini_repost", groupCallMemberActiveIcon, point(4px, 4px) }};
whoRepostedDateHeartOver: icon{{ "mediaview/mini_repost", groupCallMemberActiveIcon, point(4px, 4px) }};
whoForwardedDateHeart: icon{{ "statistics/mini_stats_share", groupCallMemberActiveIcon, point(4px, 4px) }};
whoForwardedDateHeartOver: icon{{ "statistics/mini_stats_share", groupCallMemberActiveIcon, point(4px, 4px) }};
whoReadDateChecksPosition: point(-7px, -4px);
whoReadDateStyle: TextStyle(defaultTextStyle) {
font: font(12px);

View File

@ -509,8 +509,7 @@ void WhoReactedEntryAction::setData(Data &&data) {
{ data.date },
MenuTextOptions);
}
_dateReacted = data.dateReacted;
_preloader = data.preloader;
_type = data.type;
_custom = _customEmojiFactory
? _customEmojiFactory(data.customEntityData, [=] { update(); })
: nullptr;
@ -545,13 +544,14 @@ void WhoReactedEntryAction::paint(Painter &&p) {
const auto photoSize = st::defaultWhoRead.photoSize;
const auto photoLeft = st::defaultWhoRead.photoLeft;
const auto photoTop = (height() - photoSize) / 2;
const auto preloaderBrush = _preloader
const auto preloader = (_type == WhoReactedType::Preloader);
const auto preloaderBrush = preloader
? [&] {
auto color = _st.itemFg->c;
color.setAlphaF(color.alphaF() * kPreloaderAlpha);
return QBrush(color);
}() : QBrush();
if (_preloader) {
if (preloader) {
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(preloaderBrush);
@ -568,7 +568,7 @@ void WhoReactedEntryAction::paint(Painter &&p) {
const auto textTop = withDate
? st::whoReadNameWithDateTop
: (height() - _st.itemStyle.font->height) / 2;
if (_preloader) {
if (_type == WhoReactedType::Preloader) {
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(preloaderBrush);
@ -597,10 +597,28 @@ void WhoReactedEntryAction::paint(Painter &&p) {
const auto iconPosition = QPoint(
st::defaultWhoRead.nameLeft,
st::whoReadDateTop) + st::whoReadDateChecksPosition;
const auto &icon = _dateReacted
? (selected ? st::whoLikedDateHeartOver : st::whoLikedDateHeart)
: (selected ? st::whoReadDateChecksOver : st::whoReadDateChecks);
icon.paint(p, iconPosition, width());
const auto icon = [&] {
switch (_type) {
case WhoReactedType::Viewed:
return &(selected
? st::whoReadDateChecksOver
: st::whoReadDateChecks);
case WhoReactedType::Reacted:
return &(selected
? st::whoLikedDateHeartOver
: st::whoLikedDateHeart);
case WhoReactedType::Reposted:
return &(selected
? st::whoRepostedDateHeartOver
: st::whoRepostedDateHeart);
case WhoReactedType::Forwarded:
return &(selected
? st::whoForwardedDateHeartOver
: st::whoForwardedDateHeart);
}
Unexpected("Type in WhoReactedEntryAction::paint.");
}();
icon->paint(p, iconPosition, width());
p.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);
_date.drawLeftElided(
p,
@ -708,7 +726,9 @@ void WhoReactedListMenu::populate(
append({
.text = participant.name,
.date = participant.date,
.dateReacted = participant.dateReacted,
.type = (participant.dateReacted
? WhoReactedType::Reacted
: WhoReactedType::Viewed),
.customEntityData = participant.customEntityData,
.userpic = participant.userpicLarge,
.callback = chosen,

View File

@ -54,11 +54,18 @@ struct WhoReadContent {
Fn<void(uint64)> participantChosen,
Fn<void()> showAllChosen);
enum class WhoReactedType : uchar {
Viewed,
Reacted,
Reposted,
Forwarded,
Preloader,
};
struct WhoReactedEntryData {
QString text;
QString date;
bool dateReacted = false;
bool preloader = false;
WhoReactedType type = WhoReactedType::Viewed;
QString customEntityData;
QImage userpic;
Fn<void()> callback;
@ -95,8 +102,7 @@ private:
QImage _userpic;
int _textWidth = 0;
int _customSize = 0;
bool _dateReacted = false;
bool _preloader = false;
WhoReactedType _type = WhoReactedType::Viewed;
};