Implement channel stories views / reactions.

This commit is contained in:
John Preston 2023-09-13 10:30:16 +04:00
parent 1c2951598b
commit f3db7e636b
15 changed files with 247 additions and 53 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -221,14 +221,14 @@ struct StoryUpdate {
enum class Flag : uint32 {
None = 0,
Edited = (1U << 0),
Destroyed = (1U << 1),
NewAdded = (1U << 2),
ViewsAdded = (1U << 3),
MarkRead = (1U << 4),
Reaction = (1U << 5),
Edited = (1U << 0),
Destroyed = (1U << 1),
NewAdded = (1U << 2),
ViewsChanged = (1U << 3),
MarkRead = (1U << 4),
Reaction = (1U << 5),
LastUsedBit = (1U << 5),
LastUsedBit = (1U << 5),
};
using Flags = base::flags<Flag>;
friend inline constexpr auto is_flag_type(Flag) { return true; }

View File

@ -410,8 +410,17 @@ Data::ReactionId Story::sentReactionId() const {
void Story::setReactionId(Data::ReactionId id) {
if (_sentReactionId != id) {
const auto wasEmpty = _sentReactionId.empty();
_sentReactionId = id;
session().changes().storyUpdated(this, UpdateFlag::Reaction);
auto flags = UpdateFlag::Reaction | UpdateFlag();
if (_views.known && _sentReactionId.empty() != wasEmpty) {
const auto delta = wasEmpty ? 1 : -1;
if (_views.reactions + delta >= 0) {
_views.reactions += delta;
flags |= UpdateFlag::ViewsChanged;
}
}
session().changes().storyUpdated(this, flags);
}
}
@ -438,6 +447,7 @@ void Story::applyViewsSlice(
|| (_views.total != slice.total);
_views.reactions = slice.reactions;
_views.total = slice.total;
_views.known = true;
if (offset.isEmpty()) {
_views = slice;
} else if (_views.nextOffset == offset) {
@ -468,14 +478,14 @@ void Story::applyViewsSlice(
// Count not changed, but list of recent viewers changed.
_peer->session().changes().storyUpdated(
this,
UpdateFlag::ViewsAdded);
UpdateFlag::ViewsChanged);
}
}
}
if (changed) {
_peer->session().changes().storyUpdated(
this,
UpdateFlag::ViewsAdded);
UpdateFlag::ViewsChanged);
}
}
@ -528,9 +538,11 @@ void Story::applyFields(
auto views = _views.total;
auto reactions = _views.reactions;
auto viewers = std::vector<not_null<PeerData*>>();
auto viewsKnown = _views.known;
if (const auto info = data.vviews()) {
views = info->data().vviews_count().v;
reactions = info->data().vreactions_count().value_or_empty();
viewsKnown = true;
if (const auto list = info->data().vrecent_viewers()) {
viewers.reserve(list->v.size());
auto &owner = _peer->owner();
@ -577,8 +589,14 @@ void Story::applyFields(
_edited = edited;
_pinned = pinned;
_noForwards = noForwards;
if (_views.reactions != reactions || _views.total != views) {
_views = StoryViews{ .reactions = reactions, .total = views };
if (_views.reactions != reactions
|| _views.total != views
|| _views.known != viewsKnown) {
_views = StoryViews{
.reactions = reactions,
.total = views,
.known = viewsKnown,
};
}
if (viewsChanged) {
_recentViewers = std::move(viewers);
@ -607,7 +625,7 @@ void Story::applyFields(
if (!initial && (changed || viewsChanged || reactionChanged)) {
_peer->session().changes().storyUpdated(this, UpdateFlag()
| (changed ? UpdateFlag::Edited : UpdateFlag())
| (viewsChanged ? UpdateFlag::ViewsAdded : UpdateFlag())
| (viewsChanged ? UpdateFlag::ViewsChanged : UpdateFlag())
| (reactionChanged ? UpdateFlag::Reaction : UpdateFlag()));
}
if (!initial && (captionChanged || mediaChanged)) {

View File

@ -71,6 +71,7 @@ struct StoryViews {
QString nextOffset;
int reactions = 0;
int total = 0;
bool known = false;
};
struct StoryArea {

View File

@ -937,6 +937,10 @@ ShortenedCount FormatCountToShort(int64 number) {
return result;
}
QString FormatCountDecimal(int64 number) {
return QString("%L1").arg(number);
}
PluralResult Plural(
ushort keyBase,
float64 value,
@ -973,7 +977,7 @@ PluralResult Plural(
if (type == lt_count_short) {
return { shift, shortened.string };
} else if (type == lt_count_decimal) {
return { shift, QString("%L1").arg(round) };
return { shift, FormatCountDecimal(round) };
}
return { shift, QString::number(round) };
}

View File

@ -14,13 +14,16 @@ namespace Lang {
inline constexpr auto kTextCommandLangTag = 0x20;
constexpr auto kTagReplacementSize = 4;
int FindTagReplacementPosition(const QString &original, ushort tag);
[[nodiscard]] int FindTagReplacementPosition(
const QString &original,
ushort tag);
struct ShortenedCount {
int64 number = 0;
QString string;
};
ShortenedCount FormatCountToShort(int64 number);
[[nodiscard]] ShortenedCount FormatCountToShort(int64 number);
[[nodiscard]] QString FormatCountDecimal(int64 number);
struct PluralResult {
int keyShift = 0;

View File

@ -855,24 +855,32 @@ void Controller::show(
if (!changeShown(story)) {
return;
}
_viewed = false;
invalidate_weak_ptrs(&_viewsLoadGuard);
_reactions->hide();
if (_replyArea->focused()) {
unfocusReply();
}
_replyArea->show({
.peer = unsupported ? nullptr : peer.get(),
.id = story->id(),
}, _reactions->likedValue());
const auto wasLikeButton = QPointer(_recentViews->likeButton());
_recentViews->show({
.list = story->recentViewers(),
.reactions = story->reactions(),
.total = story->views(),
.valid = peer->isSelf(),
});
.self = peer->isSelf(),
.channel = peer->isChannel(),
}, _reactions->likedValue());
if (const auto nowLikeButton = _recentViews->likeButton()) {
if (wasLikeButton != nowLikeButton) {
_reactions->attachToReactionButton(nowLikeButton);
}
}
if (peer->isSelf() || peer->isChannel()) {
_reactions->setReactionIconWidget(_recentViews->likeIconWidget());
} else if (const auto like = _replyArea->likeAnimationTarget()) {
_reactions->setReactionIconWidget(like);
}
_reactions->showLikeFrom(story);
stories.loadAround(storyId, context);
@ -906,7 +914,6 @@ bool Controller::changeShown(Data::Story *story) {
story,
Data::Stories::Polling::Viewer);
}
_reactions->showLikeFrom(story);
const auto &locations = story
? story->locations()
@ -923,6 +930,14 @@ bool Controller::changeShown(Data::Story *story) {
_areas.clear();
}
_viewed = false;
invalidate_weak_ptrs(&_viewsLoadGuard);
_reactions->hide();
_reactions->setReactionIconWidget(nullptr);
if (_replyArea->focused()) {
unfocusReply();
}
return true;
}
@ -947,7 +962,7 @@ void Controller::subscribeToSession() {
}, _sessionLifetime);
_session->changes().storyUpdates(
Data::StoryUpdate::Flag::Edited
| Data::StoryUpdate::Flag::ViewsAdded
| Data::StoryUpdate::Flag::ViewsChanged
) | rpl::filter([=](const Data::StoryUpdate &update) {
return (update.story == this->story());
}) | rpl::start_with_next([=](const Data::StoryUpdate &update) {
@ -959,7 +974,8 @@ void Controller::subscribeToSession() {
.list = update.story->recentViewers(),
.reactions = update.story->reactions(),
.total = update.story->views(),
.valid = update.story->peer()->isSelf(),
.self = update.story->peer()->isSelf(),
.channel = update.story->peer()->isChannel(),
});
}
}, _sessionLifetime);

View File

@ -445,7 +445,8 @@ void Reactions::Panel::collapse(Mode mode) {
}
}
void Reactions::Panel::attachToReactionButton(not_null<Ui::RpWidget*> button) {
void Reactions::Panel::attachToReactionButton(
not_null<Ui::RpWidget*> button) {
base::install_event_filter(button, [=](not_null<QEvent*> e) {
if (e->type() == QEvent::ContextMenu && !button->isHidden()) {
show(Reactions::Mode::Reaction);
@ -662,10 +663,17 @@ void Reactions::setReplyFieldState(
}
void Reactions::attachToReactionButton(not_null<Ui::RpWidget*> button) {
_likeButton = button;
_panel->attachToReactionButton(button);
}
void Reactions::setReactionIconWidget(Ui::RpWidget *widget) {
if (_likeIconWidget != widget) {
assignLikedId({});
_likeIconWidget = widget;
_reactionAnimation = nullptr;
}
}
auto Reactions::attachToMenu(
not_null<Ui::PopupMenu*> menu,
QPoint desiredPosition)
@ -751,7 +759,7 @@ void Reactions::ready() {
void Reactions::animateAndProcess(Chosen &&chosen) {
const auto like = (chosen.mode == Mode::Reaction);
const auto wrap = _controller->wrap();
const auto target = like ? _likeButton : wrap.get();
const auto target = like ? _likeIconWidget : wrap.get();
const auto story = _controller->story();
if (!story || !target) {
return;
@ -796,7 +804,7 @@ Fn<void(Ui::ReactionFlyCenter)> Reactions::setLikedIdIconInit(
return nullptr;
}
assignLikedId(id);
if (id.empty() || !_likeButton) {
if (id.empty() || !_likeIconWidget) {
return nullptr;
}
return crl::guard(&_likeIconGuard, [=](Ui::ReactionFlyCenter center) {
@ -812,12 +820,12 @@ void Reactions::initLikeIcon(
not_null<Data::Session*> owner,
Data::ReactionId id,
Ui::ReactionFlyCenter center) {
Expects(_likeButton != nullptr);
Expects(_likeIconWidget != nullptr);
_likeIcon = std::make_unique<Ui::RpWidget>(_likeButton);
_likeIcon = std::make_unique<Ui::RpWidget>(_likeIconWidget);
const auto icon = _likeIcon.get();
icon->show();
_likeButton->sizeValue() | rpl::start_with_next([=](QSize size) {
_likeIconWidget->sizeValue() | rpl::start_with_next([=](QSize size) {
icon->setGeometry(QRect(QPoint(), size));
}, icon->lifetime());

View File

@ -83,6 +83,7 @@ public:
rpl::producer<bool> focused,
rpl::producer<bool> hasSendText);
void attachToReactionButton(not_null<Ui::RpWidget*> button);
void setReactionIconWidget(Ui::RpWidget *widget);
using AttachStripResult = HistoryView::Reactions::AttachSelectorResult;
[[nodiscard]] AttachStripResult attachToMenu(
@ -123,7 +124,7 @@ private:
bool _replyFocused = false;
bool _hasSendText = false;
Ui::RpWidget *_likeButton = nullptr;
Ui::RpWidget *_likeIconWidget = nullptr;
rpl::variable<Data::ReactionId> _liked;
base::has_weak_ptr _likeIconGuard;
std::unique_ptr<Ui::RpWidget> _likeIcon;

View File

@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/chat/group_call_userpics.h"
#include "ui/controls/who_reacted_context_action.h"
#include "ui/layers/box_content.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/popup_menu.h"
#include "ui/painter.h"
#include "ui/rp_widget.h"
@ -128,7 +129,24 @@ RecentViews::RecentViews(not_null<Controller*> controller)
RecentViews::~RecentViews() = default;
void RecentViews::show(RecentViewsData data) {
void RecentViews::show(
RecentViewsData data,
rpl::producer<Data::ReactionId> likedValue) {
const auto guard = gsl::finally([&] {
if (_likeIcon && likedValue) {
std::move(
likedValue
) | rpl::map([](const Data::ReactionId &id) {
return !id.empty();
}) | rpl::start_with_next([=](bool liked) {
const auto icon = liked
? &st::storiesComposeControls.liked
: &st::storiesLikesIcon;
_likeIcon->setIconOverride(icon, icon);
}, _likeIcon->lifetime());
}
});
if (_data == data) {
return;
}
@ -137,27 +155,49 @@ void RecentViews::show(RecentViewsData data) {
|| (_data.reactions != data.reactions);
const auto usersChanged = !_userpics || (_data.list != data.list);
_data = data;
if (!_data.valid) {
if (!_data.self) {
_text = {};
_clickHandlerLifetime.destroy();
_userpicsLifetime.destroy();
_userpics = nullptr;
_widget = nullptr;
return;
} else {
if (!_widget) {
setupWidget();
}
if (!_userpics) {
setupUserpics();
}
if (countersChanged) {
updateText();
}
if (usersChanged) {
updateUserpics();
}
refreshClickHandler();
}
if (!_widget) {
setupWidget();
if (!_data.channel) {
_likeIcon = nullptr;
_likeWrap = nullptr;
_viewsWrap = nullptr;
} else {
_viewsCounter = Lang::FormatCountDecimal(std::max(_data.total, 1));
_likesCounter = _data.reactions
? Lang::FormatCountDecimal(_data.reactions)
: QString();
if (!_likeWrap || !_likeIcon || !_viewsWrap) {
setupViewsReactions();
}
}
if (!_userpics) {
setupUserpics();
}
if (countersChanged) {
updateText();
}
if (usersChanged) {
updateUserpics();
}
refreshClickHandler();
}
Ui::RpWidget *RecentViews::likeButton() const {
return _likeWrap.get();
}
Ui::RpWidget *RecentViews::likeIconWidget() const {
return _likeIcon.get();
}
void RecentViews::refreshClickHandler() {
@ -236,6 +276,74 @@ void RecentViews::setupWidget() {
}, raw->lifetime());
}
void RecentViews::setupViewsReactions() {
_viewsWrap = std::make_unique<Ui::RpWidget>(_controller->wrap());
_likeWrap = std::make_unique<Ui::AbstractButton>(_controller->wrap());
_likeIcon = std::make_unique<Ui::IconButton>(
_likeWrap.get(),
st::storiesComposeControls.like);
_likeIcon->setAttribute(Qt::WA_TransparentForMouseEvents);
_controller->layoutValue(
) | rpl::start_with_next([=](const Layout &layout) {
_outer = layout.views;
updateViewsReactionsGeometry();
}, _likeWrap->lifetime());
const auto views = Ui::CreateChild<Ui::FlatLabel>(
_viewsWrap.get(),
_viewsCounter.value(),
st::storiesViewsText);
views->show();
views->setAttribute(Qt::WA_TransparentForMouseEvents);
views->move(st::storiesViewsTextPosition);
views->widthValue(
) | rpl::start_with_next([=](int width) {
_viewsWrap->resize(views->x() + width, _likeIcon->height());
updateViewsReactionsGeometry();
}, _viewsWrap->lifetime());
_viewsWrap->paintRequest() | rpl::start_with_next([=] {
auto p = QPainter(_viewsWrap.get());
const auto &icon = st::storiesViewsIcon;
const auto top = (_viewsWrap->height() - icon.height()) / 2;
icon.paint(p, 0, top, _viewsWrap->width());
}, _viewsWrap->lifetime());
_likeIcon->move(0, 0);
const auto likes = Ui::CreateChild<Ui::FlatLabel>(
_likeWrap.get(),
_likesCounter.value(),
st::storiesLikesText);
likes->show();
likes->setAttribute(Qt::WA_TransparentForMouseEvents);
likes->move(st::storiesLikesTextPosition);
likes->widthValue(
) | rpl::start_with_next([=](int width) {
width += width
? st::storiesLikesTextRightSkip
: st::storiesLieksEmptyRightSkip;
_likeWrap->resize(likes->x() + width, _likeIcon->height());
updateViewsReactionsGeometry();
}, _likeWrap->lifetime());
_viewsWrap->show();
_likeIcon->show();
_likeWrap->show();
_likeWrap->setClickedCallback([=] {
_controller->toggleLiked();
});
}
void RecentViews::updateViewsReactionsGeometry() {
_viewsWrap->move(_outer.topLeft() + st::storiesViewsPosition);
_likeWrap->move(_outer.topLeft()
+ QPoint(_outer.width() - _likeWrap->width(), 0)
+ st::storiesLikesPosition);
}
void RecentViews::updatePartsGeometry() {
const auto skip = st::storiesRecentViewsSkip;
const auto full = _userpicsWidth + skip + _text.maxWidth();

View File

@ -13,9 +13,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Data {
struct StoryView;
struct ReactionId;
} // namespace Data
namespace Ui {
class AbstractButton;
class IconButton;
class RpWidget;
class GroupCallUserpics;
class PopupMenu;
@ -34,7 +37,8 @@ struct RecentViewsData {
std::vector<not_null<PeerData*>> list;
int reactions = 0;
int total = 0;
bool valid = false;
bool self = false;
bool channel = false;
friend inline auto operator<=>(
const RecentViewsData &,
@ -49,7 +53,12 @@ public:
explicit RecentViews(not_null<Controller*> controller);
~RecentViews();
void show(RecentViewsData data);
void show(
RecentViewsData data,
rpl::producer<Data::ReactionId> likedValue = nullptr);
[[nodiscard]] Ui::RpWidget *likeButton() const;
[[nodiscard]] Ui::RpWidget *likeIconWidget() const;
private:
struct MenuEntry {
@ -69,6 +78,9 @@ private:
void updatePartsGeometry();
void showMenu();
void setupViewsReactions();
void updateViewsReactionsGeometry();
void addMenuRow(Data::StoryView entry, const QDateTime &now);
void addMenuRowPlaceholder(not_null<Main::Session*> session);
void rebuildMenuTail();
@ -83,6 +95,12 @@ private:
RecentViewsData _data;
rpl::lifetime _userpicsLifetime;
rpl::variable<QString> _viewsCounter;
rpl::variable<QString> _likesCounter;
std::unique_ptr<Ui::RpWidget> _viewsWrap;
std::unique_ptr<Ui::AbstractButton> _likeWrap;
std::unique_ptr<Ui::IconButton> _likeIcon;
base::unique_qptr<Ui::PopupMenu> _menu;
rpl::lifetime _menuShortLifetime;
std::vector<MenuEntry> _menuEntries;

View File

@ -991,3 +991,20 @@ storiesStealthBoxBottom: 11px;
storiesStealthToast: Toast(defaultMultilineToast) {
maxWidth: 340px;
}
storiesViewsPosition: point(4px, 29px);
storiesViewsIcon: icon{{ "mediaview/views", storiesComposeGrayText }};
storiesViewsText: FlatLabel(defaultFlatLabel) {
textFg: storiesComposeGrayText;
}
storiesViewsTextPosition: point(26px, 14px);
storiesLikesPosition: point(0px, 29px);
storiesLikesIcon: icon {{ "chat/input_like", storiesComposeWhiteText }};
storiesLikesText: FlatLabel(defaultFlatLabel) {
textFg: storiesComposeWhiteText;
style: semiboldTextStyle;
}
storiesLikesTextPosition: point(41px, 14px);
storiesLikesTextRightSkip: 8px;
storiesLieksEmptyRightSkip: 2px;

View File

@ -213,7 +213,7 @@ void EditInviteLinkBox(
? tr::lng_group_invite_usage_any(tr::now)
: !limit
? tr::lng_group_invite_usage_custom(tr::now)
: QString("%L1").arg(limit);
: Lang::FormatCountDecimal(limit);
state->usageButtons.emplace(
limit,
addButton(usagesWrap, usageGroup, limit, text));