Load and show list of users who viewed a story.

This commit is contained in:
John Preston 2023-06-01 20:01:29 +04:00
parent 16069db3e6
commit d28bd36d22
39 changed files with 784 additions and 200 deletions

View File

@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/controls/who_reacted_context_action.h"
#include "apiwrap.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
namespace Api {
namespace {
@ -357,37 +358,6 @@ struct State {
});
}
[[nodiscard]] QString FormatReadDate(TimeId date, const QDateTime &now) {
if (!date) {
return {};
}
const auto parsed = base::unixtime::parse(date);
const auto readDate = parsed.date();
const auto nowDate = now.date();
if (readDate == nowDate) {
return tr::lng_mediaview_today(
tr::now,
lt_time,
QLocale().toString(parsed.time(), QLocale::ShortFormat));
} else if (readDate.addDays(1) == nowDate) {
return tr::lng_mediaview_yesterday(
tr::now,
lt_time,
QLocale().toString(parsed.time(), QLocale::ShortFormat));
}
return tr::lng_mediaview_date_time(
tr::now,
lt_date,
tr::lng_month_day(
tr::now,
lt_month,
Lang::MonthDay(readDate.month())(tr::now),
lt_day,
QString::number(readDate.day())),
lt_time,
QLocale().toString(parsed.time(), QLocale::ShortFormat));
}
bool UpdateUserpics(
not_null<State*> state,
not_null<HistoryItem*> item,
@ -614,6 +584,37 @@ rpl::producer<Ui::WhoReadContent> WhoReacted(
} // namespace
QString FormatReadDate(TimeId date, const QDateTime &now) {
if (!date) {
return {};
}
const auto parsed = base::unixtime::parse(date);
const auto readDate = parsed.date();
const auto nowDate = now.date();
if (readDate == nowDate) {
return tr::lng_mediaview_today(
tr::now,
lt_time,
QLocale().toString(parsed.time(), QLocale::ShortFormat));
} else if (readDate.addDays(1) == nowDate) {
return tr::lng_mediaview_yesterday(
tr::now,
lt_time,
QLocale().toString(parsed.time(), QLocale::ShortFormat));
}
return tr::lng_mediaview_date_time(
tr::now,
lt_date,
tr::lng_month_day(
tr::now,
lt_month,
Lang::MonthDay(readDate.month())(tr::now),
lt_day,
QString::number(readDate.day())),
lt_time,
QLocale().toString(parsed.time(), QLocale::ShortFormat));
}
bool WhoReadExists(not_null<HistoryItem*> item) {
if (!item->out()) {
return false;

View File

@ -29,6 +29,7 @@ enum class WhoReactedList {
One,
};
[[nodiscard]] QString FormatReadDate(TimeId date, const QDateTime &now);
[[nodiscard]] bool WhoReadExists(not_null<HistoryItem*> item);
[[nodiscard]] bool WhoReactedExists(
not_null<HistoryItem*> item,

View File

@ -34,7 +34,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/abstract_box.h"
#include "base/timer.h"
#include "styles/style_calls.h"
#include "styles/style_chat.h" // style::GroupCallUserpics
#include "styles/style_chat_helpers.h" // style::GroupCallUserpics
#include "styles/style_layers.h"
namespace Calls {

View File

@ -201,6 +201,31 @@ ComposeControls {
premium: PremiumLimits;
}
WhoRead {
userpics: GroupCallUserpics;
photoLeft: pixels;
photoSize: pixels;
photoSkip: pixels;
nameLeft: pixels;
iconPosition: point;
itemPadding: margins;
}
defaultWhoRead: WhoRead {
userpics: GroupCallUserpics {
size: 22px;
shift: 8px;
stroke: 4px;
align: align(right);
}
photoLeft: 13px;
photoSize: 30px;
photoSkip: 5px;
nameLeft: 57px;
iconPosition: point(15px, 7px);
itemPadding: margins(44px, 9px, 17px, 7px);
}
switchPmButton: RoundButton(defaultBoxButton) {
width: 320px;
height: 34px;
@ -1091,3 +1116,21 @@ defaultComposeControls: ComposeControls {
files: defaultComposeFiles;
premium: defaultPremiumLimits;
}
moreChatsBarHeight: 48px;
moreChatsBarTextPosition: point(12px, 4px);
moreChatsBarStatusPosition: point(12px, 24px);
moreChatsBarClose: IconButton(defaultIconButton) {
width: 48px;
height: 48px;
icon: boxTitleCloseIcon;
iconOver: boxTitleCloseIconOver;
iconPosition: point(12px, -1px);
rippleAreaPosition: point(0px, 4px);
rippleAreaSize: 40px;
ripple: RippleAnimation(defaultRippleAnimation) {
color: windowBgOver;
}
}

View File

@ -181,10 +181,48 @@ const std::vector<not_null<PeerData*>> &Story::recentViewers() const {
return _recentViewers;
}
const std::vector<StoryView> &Story::viewsList() const {
return _viewsList;
}
int Story::views() const {
return _views;
}
void Story::applyViewsSlice(
const std::optional<StoryView> &offset,
const std::vector<StoryView> &slice,
int total) {
_views = total;
if (!offset) {
const auto i = _viewsList.empty()
? end(slice)
: ranges::find(slice, _viewsList.front());
const auto merge = (i != end(slice))
&& !ranges::contains(slice, _viewsList.back());
if (merge) {
_viewsList.insert(begin(_viewsList), begin(slice), i);
} else {
_viewsList = slice;
}
} else if (!slice.empty()) {
const auto i = ranges::find(_viewsList, *offset);
const auto merge = (i != end(_viewsList))
&& !ranges::contains(_viewsList, slice.back());
if (merge) {
const auto after = i + 1;
if (after == end(_viewsList)) {
_viewsList.insert(after, begin(slice), end(slice));
} else {
const auto j = ranges::find(slice, _viewsList.back());
if (j != end(slice)) {
_viewsList.insert(end(_viewsList), j + 1, end(slice));
}
}
}
}
}
bool Story::applyChanges(StoryMedia media, const MTPDstoryItem &data) {
const auto pinned = data.is_pinned();
auto caption = TextWithEntities{
@ -683,6 +721,56 @@ void Stories::sendMarkAsReadRequests() {
}
}
void Stories::loadViewsSlice(
StoryId id,
std::optional<StoryView> offset,
Fn<void(std::vector<StoryView>)> done) {
_viewsDone = std::move(done);
if (_viewsStoryId == id && _viewsOffset == offset) {
return;
}
_viewsStoryId = id;
_viewsOffset = offset;
const auto api = &_owner->session().api();
api->request(_viewsRequestId).cancel();
_viewsRequestId = api->request(MTPstories_GetStoryViewsList(
MTP_int(id),
MTP_int(offset ? offset->date : 0),
MTP_long(offset ? peerToUser(offset->peer->id).bare : 0),
MTP_int(2)
)).done([=](const MTPstories_StoryViewsList &result) {
_viewsRequestId = 0;
auto slice = std::vector<StoryView>();
const auto &data = result.data();
_owner->processUsers(data.vusers());
slice.reserve(data.vviews().v.size());
for (const auto &view : data.vviews().v) {
slice.push_back({
.peer = _owner->peer(peerFromUser(view.data().vuser_id())),
.date = view.data().vdate().v,
});
}
const auto fullId = FullStoryId{
.peer = _owner->session().userPeerId(),
.story = _viewsStoryId,
};
if (const auto story = lookup(fullId)) {
(*story)->applyViewsSlice(_viewsOffset, slice, data.vcount().v);
}
if (const auto done = base::take(_viewsDone)) {
done(std::move(slice));
}
}).fail([=] {
_viewsRequestId = 0;
if (const auto done = base::take(_viewsDone)) {
done({});
}
}).send();
}
bool Stories::isQuitPrevent() {
if (!_markReadPending.empty()) {
sendMarkAsReadRequests();

View File

@ -28,6 +28,13 @@ struct StoryMedia {
friend inline bool operator==(StoryMedia, StoryMedia) = default;
};
struct StoryView {
not_null<PeerData*> peer;
TimeId date = 0;
friend inline bool operator==(StoryView, StoryView) = default;
};
class Story {
public:
Story(
@ -60,7 +67,12 @@ public:
void setViewsData(std::vector<not_null<PeerData*>> recent, int total);
[[nodiscard]] auto recentViewers() const
-> const std::vector<not_null<PeerData*>> &;
[[nodiscard]] const std::vector<StoryView> &viewsList() const;
[[nodiscard]] int views() const;
void applyViewsSlice(
const std::optional<StoryView> &offset,
const std::vector<StoryView> &slice,
int total);
bool applyChanges(StoryMedia media, const MTPDstoryItem &data);
@ -70,6 +82,7 @@ private:
StoryMedia _media;
TextWithEntities _caption;
std::vector<not_null<PeerData*>> _recentViewers;
std::vector<StoryView> _viewsList;
int _views = 0;
const TimeId _date = 0;
bool _pinned = false;
@ -124,6 +137,12 @@ public:
[[nodiscard]] bool isQuitPrevent();
void markAsRead(FullStoryId id, bool viewed);
static constexpr auto kViewsPerPage = 50;
void loadViewsSlice(
StoryId id,
std::optional<StoryView> offset,
Fn<void(std::vector<StoryView>)> done);
private:
[[nodiscard]] StoriesList parse(const MTPUserStories &stories);
[[nodiscard]] Story *parseAndApply(
@ -172,6 +191,11 @@ private:
base::Timer _markReadTimer;
base::flat_set<PeerId> _markReadRequests;
StoryId _viewsStoryId = 0;
std::optional<StoryView> _viewsOffset;
Fn<void(std::vector<StoryView>)> _viewsDone;
mtpRequestId _viewsRequestId = 0;
};
} // namespace Data

View File

@ -67,6 +67,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "info/info_memento.h"
#include "styles/style_dialogs.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_info.h"
#include "styles/style_window.h"
#include "base/qt/qt_common_adapters.h"

View File

@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h"
#include "lang/lang_keys.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_window.h"
#include "styles/style_info.h"

View File

@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_dialogs.h"
#include "styles/style_info.h"

View File

@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_peer_menu.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
namespace HistoryView::Controls {
namespace {

View File

@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "menu/menu_ttl_validator.h"
#include "ui/text/format_values.h"
#include "ui/text/text_utilities.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_chat.h"
namespace HistoryView::Controls {

View File

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/paint/blobs.h"
#include "ui/painter.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_layers.h"
namespace HistoryView::Controls {

View File

@ -40,6 +40,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/unixtime.h"
#include "boxes/peers/edit_contact_box.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_layers.h"
#include "styles/style_info.h"
#include "styles/style_menu_icons.h"

View File

@ -68,6 +68,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "spellcheck/spellcheck_types.h"
#include "apiwrap.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_menu_icons.h"
#include <QtGui/QGuiApplication>

View File

@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h"
#include "ui/toast/toast.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
namespace HistoryView {

View File

@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "calls/calls_instance.h"
#include "core/application.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
namespace HistoryView {

View File

@ -45,6 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "apiwrap.h"
#include "styles/style_widgets.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_dialogs.h"
namespace HistoryView {

View File

@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/weak_ptr.h"
#include "apiwrap.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
namespace HistoryView {
namespace {

View File

@ -46,6 +46,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "platform/platform_specific.h"
#include "lang/lang_keys.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_window.h"
#include "styles/style_info.h"
#include "styles/style_boxes.h"

View File

@ -85,6 +85,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "info/profile/info_profile_values.h"
#include "lang/lang_keys.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_window.h"
#include "styles/style_info.h"
#include "styles/style_boxes.h"

View File

@ -58,6 +58,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "inline_bots/inline_bot_result.h"
#include "lang/lang_keys.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_window.h"
#include "styles/style_info.h"
#include "styles/style_boxes.h"

View File

@ -284,7 +284,7 @@ void TranslateBar::setup(not_null<History*> history) {
button->paintRequest(
) | rpl::start_with_next([=](QRect clip) {
QPainter(button).fillRect(clip, st::historyComposeButton.bgColor);
QPainter(button).fillRect(clip, st::historyComposeButtonBg);
}, button->lifetime());
button->setClickedCallback([=] {

View File

@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
namespace HistoryView::Reactions {
namespace {

View File

@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/menu/menu_action.h"
#include "ui/widgets/popup_menu.h"
#include "styles/style_chat.h" // expandedMenuSeparator.
#include "styles/style_chat_helpers.h"
namespace Info {
namespace Profile {

View File

@ -426,7 +426,7 @@ void Controller::show(
_shown = storyId;
_captionText = story->caption();
_captionFullView = nullptr;
invalidate_weak_ptrs(&_viewsLoadGuard);
if (_replyFocused) {
unfocusReply();
}
@ -680,6 +680,96 @@ SiblingView Controller::sibling(SiblingType type) const {
return {};
}
ViewsSlice Controller::views(PeerId offset) {
invalidate_weak_ptrs(&_viewsLoadGuard);
if (!offset) {
refreshViewsFromData();
} else if (!sliceViewsTo(offset)) {
return { .left = _viewsSlice.left };
}
return _viewsSlice;
}
rpl::producer<> Controller::moreViewsLoaded() const {
return _moreViewsLoaded.events();
}
Fn<void(std::vector<Data::StoryView>)> Controller::viewsGotMoreCallback() {
return crl::guard(&_viewsLoadGuard, [=](
const std::vector<Data::StoryView> &result) {
if (_viewsSlice.list.empty()) {
auto &stories = _list->user->owner().stories();
if (const auto maybeStory = stories.lookup(_shown)) {
_viewsSlice = {
.list = result,
.left = (*maybeStory)->views() - int(result.size()),
};
} else {
_viewsSlice = {};
}
} else {
_viewsSlice.list.insert(
end(_viewsSlice.list),
begin(result),
end(result));
_viewsSlice.left
= std::max(_viewsSlice.left - int(result.size()), 0);
}
_moreViewsLoaded.fire({});
});
}
void Controller::refreshViewsFromData() {
Expects(_list.has_value());
auto &stories = _list->user->owner().stories();
const auto maybeStory = stories.lookup(_shown);
if (!maybeStory || !_list->user->isSelf()) {
_viewsSlice = {};
return;
}
const auto story = *maybeStory;
const auto &list = story->viewsList();
const auto total = story->views();
_viewsSlice.list = list
| ranges::views::take(Data::Stories::kViewsPerPage)
| ranges::to_vector;
_viewsSlice.left = total - int(_viewsSlice.list.size());
if (_viewsSlice.list.empty() && _viewsSlice.left > 0) {
const auto done = viewsGotMoreCallback();
stories.loadViewsSlice(_shown.story, std::nullopt, done);
}
}
bool Controller::sliceViewsTo(PeerId offset) {
Expects(_list.has_value());
auto &stories = _list->user->owner().stories();
const auto maybeStory = stories.lookup(_shown);
if (!maybeStory || !_list->user->isSelf()) {
_viewsSlice = {};
return true;
}
const auto story = *maybeStory;
const auto &list = story->viewsList();
const auto proj = [&](const Data::StoryView &single) {
return single.peer->id;
};
const auto i = ranges::find(list, _viewsSlice.list.back());
const auto add = (i != end(list)) ? int(end(list) - i - 1) : 0;
const auto j = ranges::find(_viewsSlice.list, offset, proj);
Assert(j != end(_viewsSlice.list));
if (!add && (j + 1) == end(_viewsSlice.list)) {
const auto done = viewsGotMoreCallback();
stories.loadViewsSlice(_shown.story, _viewsSlice.list.back(), done);
return false;
}
_viewsSlice.list.erase(begin(_viewsSlice.list), j + 1);
_viewsSlice.list.insert(end(_viewsSlice.list), i + 1, end(list));
_viewsSlice.left -= add;
return true;
}
void Controller::unfocusReply() {
_wrap->setFocus();
}

View File

@ -78,6 +78,11 @@ struct Layout {
friend inline bool operator==(Layout, Layout) = default;
};
struct ViewsSlice {
std::vector<Data::StoryView> list;
int left = 0;
};
class Controller final {
public:
explicit Controller(not_null<Delegate*> delegate);
@ -114,6 +119,9 @@ public:
void repaintSibling(not_null<Sibling*> sibling);
[[nodiscard]] SiblingView sibling(SiblingType type) const;
[[nodiscard]] ViewsSlice views(PeerId offset);
[[nodiscard]] rpl::producer<> moreViewsLoaded() const;
void unfocusReply();
[[nodiscard]] rpl::lifetime &lifetime();
@ -142,6 +150,11 @@ private:
void subjumpTo(int index);
void checkWaitingFor();
void refreshViewsFromData();
bool sliceViewsTo(PeerId offset);
[[nodiscard]] auto viewsGotMoreCallback()
-> Fn<void(std::vector<Data::StoryView>)>;
const not_null<Delegate*> _delegate;
rpl::variable<std::optional<Layout>> _layout;
@ -170,6 +183,10 @@ private:
int _index = 0;
bool _started = false;
ViewsSlice _viewsSlice;
rpl::event_stream<> _moreViewsLoaded;
base::has_weak_ptr _viewsLoadGuard;
std::unique_ptr<Sibling> _siblingLeft;
std::unique_ptr<Sibling> _siblingRight;

View File

@ -7,11 +7,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "media/stories/media_stories_recent_views.h"
#include "api/api_who_reacted.h" // FormatReadDate.
#include "data/data_peer.h"
#include "data/data_stories.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/widgets/popup_menu.h"
#include "ui/controls/who_reacted_context_action.h"
#include "ui/painter.h"
#include "ui/rp_widget.h"
#include "ui/userpic_view.h"
@ -21,6 +25,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Media::Stories {
namespace {
constexpr auto kAddPerPage = 50;
constexpr auto kLoadViewsPages = 2;
[[nodiscard]] rpl::producer<std::vector<Ui::GroupCallUser>> ContentByUsers(
const std::vector<not_null<PeerData*>> &list) {
struct Userpic {
@ -37,7 +44,7 @@ namespace {
bool scheduled = false;
};
static const auto size = st::storiesRecentViewsUserpics.size;
static const auto size = st::storiesWhoViewed.userpics.size;
static const auto GenerateUserpic = [](Userpic &userpic) {
auto result = userpic.peer->generateUserpicImage(
@ -132,57 +139,302 @@ void RecentViews::show(RecentViewsData data) {
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));
setupWidget();
}
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());
setupUserpics();
}
if (totalChanged) {
updateText();
}
if (usersChanged) {
_userpicsLifetime = ContentByUsers(
data.list
) | rpl::start_with_next([=](
const std::vector<Ui::GroupCallUser> &list) {
_userpics->update(list, true);
});
updateUserpics();
}
}
void RecentViews::updateUserpics() {
_userpicsLifetime = ContentByUsers(
_data.list
) | rpl::start_with_next([=](
const std::vector<Ui::GroupCallUser> &list) {
_userpics->update(list, true);
});
}
void RecentViews::setupUserpics() {
_userpics = std::make_unique<Ui::GroupCallUserpics>(
st::storiesWhoViewed.userpics,
rpl::single(true),
[=] { _widget->update(); });
_userpics->widthValue() | rpl::start_with_next([=](int width) {
if (_userpicsWidth != width) {
_userpicsWidth = width;
updatePartsGeometry();
}
}, _widget->lifetime());
}
void RecentViews::setupWidget() {
_widget = std::make_unique<Ui::RpWidget>(_controller->wrap());
const auto raw = _widget.get();
raw->show();
_controller->layoutValue(
) | rpl::start_with_next([=](const Layout &layout) {
_outer = layout.views;
updatePartsGeometry();
}, raw->lifetime());
raw->paintRequest(
) | rpl::start_with_next([=] {
auto p = Painter(raw);
_userpics->paint(
p,
_userpicsPosition.x(),
_userpicsPosition.y(),
st::storiesWhoViewed.userpics.size);
p.setPen(st::storiesComposeWhiteText);
_text.drawElided(
p,
_textPosition.x(),
_textPosition.y(),
raw->width() - _userpicsWidth - st::storiesRecentViewsSkip);
}, raw->lifetime());
raw->events(
) | rpl::filter([=](not_null<QEvent*> e) {
return (_data.total > 0)
&& (e->type() == QEvent::MouseButtonPress)
&& (static_cast<QMouseEvent*>(e.get())->button()
== Qt::LeftButton);
}) | rpl::start_with_next([=] {
showMenu();
}, raw->lifetime());
raw->setCursor(style::cur_pointer);
}
void RecentViews::updatePartsGeometry() {
const auto skip = st::storiesRecentViewsSkip;
const auto full = _userpicsWidth + skip + _text.maxWidth();
const auto use = std::min(full, _outer.width());
const auto ux = _outer.x() + (_outer.width() - use) / 2;
const auto uheight = st::storiesWhoViewed.userpics.size;
const auto uy = _outer.y() + (_outer.height() - uheight) / 2;
const auto tx = ux + _userpicsWidth + skip;
const auto theight = st::normalFont->height;
const auto ty = _outer.y() + (_outer.height() - theight) / 2;
const auto my = std::min(uy, ty);
const auto mheight = std::max(uheight, theight);
const auto padding = skip;
_userpicsPosition = QPoint(padding, uy - my);
_textPosition = QPoint(tx - ux + padding, ty - my);
_widget->setGeometry(ux - padding, my, use + 2 * padding, mheight);
_widget->update();
}
void RecentViews::updateText() {
_text.setText(st::defaultTextStyle, _data.total
? tr::lng_stories_views(tr::now, lt_count, _data.total)
: tr::lng_stories_no_views(tr::now));
updatePartsGeometry();
}
void RecentViews::showMenu() {
if (_menu) {
return;
}
const auto views = _controller->views(PeerId());
if (views.list.empty() && !views.left) {
return;
}
using namespace Ui;
_menuShortLifetime.destroy();
_menu = base::make_unique_q<PopupMenu>(
_widget.get(),
st::storiesViewsMenu);
auto count = 0;
const auto added = std::min(int(views.list.size()), kAddPerPage);
const auto add = std::min(added + views.left, kAddPerPage);
const auto now = QDateTime::currentDateTime();
for (const auto &entry : views.list) {
addMenuRow(entry, now);
if (++count >= add) {
break;
}
}
while (count++ < add) {
addMenuRowPlaceholder();
}
rpl::merge(
_controller->moreViewsLoaded(),
rpl::combine(
_menu->scrollTopValue(),
_menuEntriesCount.value()
) | rpl::filter([=](int scrollTop, int count) {
const auto fullHeight = count
* (st::defaultWhoRead.photoSkip * 2
+ st::defaultWhoRead.photoSize);
return fullHeight
< (scrollTop
+ st::storiesViewsMenu.maxHeight * kLoadViewsPages);
}) | rpl::to_empty
) | rpl::start_with_next([=] {
rebuildMenuTail();
}, _menuShortLifetime);
_menu->setDestroyedCallback(crl::guard(_widget.get(), [=] {
_menuShortLifetime.destroy();
_menuEntries.clear();
_menuEntriesCount = 0;
_menuPlaceholderCount = 0;
}));
const auto size = _menu->size();
const auto geometry = _widget->mapToGlobal(_widget->rect());
_menu->setForcedVerticalOrigin(PopupMenu::VerticalOrigin::Bottom);
_menu->popup(QPoint(
geometry.x() + (_widget->width() - size.width()) / 2,
geometry.y() + _widget->height()));
_menuEntriesCount = _menuEntriesCount.current() + added;
}
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 prepare = [&](Ui::PeerUserpicView &view) {
const auto size = st::storiesWhoViewed.photoSize;
auto userpic = peer->generateUserpicImage(
view,
size * style::DevicePixelRatio());
userpic.setDevicePixelRatio(style::DevicePixelRatio());
return Ui::WhoReactedEntryData{
.text = peer->name(),
.date = date,
.userpic = std::move(userpic),
.callback = [] {},
};
};
if (_menuPlaceholderCount > 0) {
const auto i = _menuEntries.end() - (_menuPlaceholderCount--);
i->peer = peer;
i->date = date;
i->action->setData(prepare(i->view));
} else {
auto view = Ui::PeerUserpicView();
auto action = base::make_unique_q<Ui::WhoReactedEntryAction>(
_menu->menu(),
nullptr,
_menu->menu()->st(),
prepare(view));
const auto raw = action.get();
_menu->addAction(std::move(action));
_menuEntries.push_back({
.action = raw,
.peer = peer,
.date = date,
.view = std::move(view),
});
}
const auto i = end(_menuEntries) - _menuPlaceholderCount - 1;
i->key = peer->userpicUniqueKey(i->view);
if (peer->hasUserpic() && peer->useEmptyUserpic(i->view)) {
if (_waitingForUserpics.emplace(i - begin(_menuEntries)).second
&& _waitingForUserpics.size() == 1) {
subscribeToMenuUserpicsLoading(&peer->session());
}
}
}
void RecentViews::addMenuRowPlaceholder() {
auto action = base::make_unique_q<Ui::WhoReactedEntryAction>(
_menu->menu(),
nullptr,
_menu->menu()->st(),
Ui::WhoReactedEntryData{ .preloader = true });
const auto raw = action.get();
_menu->addAction(std::move(action));
_menuEntries.push_back({ .action = raw });
++_menuPlaceholderCount;
}
void RecentViews::rebuildMenuTail() {
const auto offset = (_menuPlaceholderCount < _menuEntries.size())
? (end(_menuEntries) - _menuPlaceholderCount - 1)->peer->id
: PeerId();
const auto views = _controller->views(offset);
if (views.list.empty()) {
return;
}
const auto now = QDateTime::currentDateTime();
const auto added = std::min(
_menuPlaceholderCount + kAddPerPage,
int(views.list.size()));
auto add = added;
for (const auto &entry : views.list) {
addMenuRow(entry, now);
if (!--add) {
break;
}
}
_menuEntriesCount = _menuEntriesCount.current() + added;
}
void RecentViews::subscribeToMenuUserpicsLoading(
not_null<Main::Session*> session) {
_shortAnimationPlaying = style::ShortAnimationPlaying();
_waitingForUserpicsLifetime = rpl::merge(
_shortAnimationPlaying.changes() | rpl::filter([=](bool playing) {
return !playing && _waitingUserpicsCheck;
}) | rpl::to_empty,
session->downloaderTaskFinished(
) | rpl::filter([=] {
if (_shortAnimationPlaying.current()) {
_waitingUserpicsCheck = true;
return false;
}
return true;
})
) | rpl::start_with_next([=] {
_waitingUserpicsCheck = false;
for (auto i = begin(_waitingForUserpics)
; i != end(_waitingForUserpics)
;) {
auto &entry = _menuEntries[*i];
auto &view = entry.view;
const auto peer = entry.peer;
const auto key = peer->userpicUniqueKey(view);
const auto update = (entry.key != key);
if (update) {
const auto size = st::storiesWhoViewed.photoSize;
auto userpic = peer->generateUserpicImage(
view,
size * style::DevicePixelRatio());
userpic.setDevicePixelRatio(style::DevicePixelRatio());
entry.action->setData({
.text = peer->name(),
.date = entry.date,
.userpic = std::move(userpic),
.callback = [] {},
});
entry.key = key;
if (!peer->hasUserpic() || !peer->useEmptyUserpic(view)) {
i = _waitingForUserpics.erase(i);
continue;
}
}
++i;
}
if (_waitingForUserpics.empty()) {
_waitingForUserpicsLifetime.destroy();
}
});
}
} // namespace Media::Stories

View File

@ -7,13 +7,25 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/unique_qptr.h"
#include "ui/text/text.h"
#include "ui/userpic_view.h"
namespace Data {
struct StoryView;
} // namespace Data
namespace Ui {
class RpWidget;
class GroupCallUserpics;
class PopupMenu;
class WhoReactedEntryAction;
} // namespace Ui
namespace Main {
class Session;
} // namespace Main
namespace Media::Stories {
class Controller;
@ -39,6 +51,26 @@ public:
void show(RecentViewsData data);
private:
struct MenuEntry {
not_null<Ui::WhoReactedEntryAction*> action;
PeerData *peer = nullptr;
QString date;
Ui::PeerUserpicView view;
InMemoryKey key;
};
void setupWidget();
void setupUserpics();
void updateUserpics();
void updateText();
void updatePartsGeometry();
void showMenu();
void addMenuRow(Data::StoryView entry, const QDateTime &now);
void addMenuRowPlaceholder();
void rebuildMenuTail();
void subscribeToMenuUserpicsLoading(not_null<Main::Session*> session);
const not_null<Controller*> _controller;
std::unique_ptr<Ui::RpWidget> _widget;
@ -46,6 +78,20 @@ private:
Ui::Text::String _text;
RecentViewsData _data;
rpl::lifetime _userpicsLifetime;
base::unique_qptr<Ui::PopupMenu> _menu;
rpl::lifetime _menuShortLifetime;
std::vector<MenuEntry> _menuEntries;
rpl::variable<int> _menuEntriesCount = 0;
int _menuPlaceholderCount = 0;
base::flat_set<int> _waitingForUserpics;
rpl::variable<bool> _shortAnimationPlaying;
bool _waitingUserpicsCheck = false;
rpl::lifetime _waitingForUserpicsLifetime;
QRect _outer;
QPoint _userpicsPosition;
QPoint _textPosition;
int _userpicsWidth = 0;
};

View File

@ -729,11 +729,21 @@ storiesComposeControls: ComposeControls(defaultComposeControls) {
}
premium: storiesComposePremium;
}
storiesRecentViewsUserpics: GroupCallUserpics {
size: 24px;
shift: 9px;
stroke: 4px;
align: align(left);
storiesViewsMenu: PopupMenu(storiesPopupMenuWithIcons) {
scrollPadding: margins(0px, 6px, 0px, 4px);
maxHeight: 320px;
menu: Menu(storiesMenuWithIcons) {
widthMin: 215px;
widthMax: 215px;
}
radius: 7px;
}
storiesRecentViewsSkip: 8px;
storiesWhoViewed: WhoRead(defaultWhoRead) {
userpics: GroupCallUserpics {
size: 24px;
shift: 9px;
stroke: 4px;
align: align(left);
}
}

View File

@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "mainwindow.h"
#include "windows_quiethours_h.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include <QtCore/QOperatingSystemVersion>

View File

@ -54,6 +54,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_boxes.h"
#include "styles/style_settings.h"
#include "styles/style_info.h"

View File

@ -727,29 +727,6 @@ popupMenuExpandedSeparator: PopupMenu(popupMenuWithIcons) {
}
}
WhoRead {
userpics: GroupCallUserpics;
photoLeft: pixels;
photoSize: pixels;
photoSkip: pixels;
nameLeft: pixels;
iconPosition: point;
itemPadding: margins;
}
defaultWhoRead: WhoRead {
userpics: GroupCallUserpics {
size: 22px;
shift: 8px;
stroke: 4px;
align: align(right);
}
photoLeft: 13px;
photoSize: 30px;
photoSkip: 5px;
nameLeft: 57px;
iconPosition: point(15px, 7px);
itemPadding: margins(44px, 9px, 17px, 7px);
}
whoReadMenu: PopupMenu(popupMenuExpandedSeparator) {
scrollPadding: margins(0px, 6px, 0px, 4px);
maxHeight: 400px;
@ -934,24 +911,6 @@ historySendDisabledIcon: icon {{ "emoji/premium_lock", placeholderFgActive }};
historySendDisabledIconSkip: 20px;
historySendDisabledPosition: point(0px, 0px);
moreChatsBarHeight: 48px;
moreChatsBarTextPosition: point(12px, 4px);
moreChatsBarStatusPosition: point(12px, 24px);
moreChatsBarClose: IconButton(defaultIconButton) {
width: 48px;
height: 48px;
icon: boxTitleCloseIcon;
iconOver: boxTitleCloseIconOver;
iconPosition: point(12px, -1px);
rippleAreaPosition: point(0px, 4px);
rippleAreaSize: 40px;
ripple: RippleAnimation(defaultRippleAnimation) {
color: windowBgOver;
}
}
backgroundSwitchToDark: IconButton(defaultIconButton) {
width: 48px;
height: 48px;

View File

@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h"
#include "base/unixtime.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_calls.h"
#include "styles/style_info.h" // st::topBarArrowPadding, like TopBarWidget.
#include "styles/style_window.h" // st::columnMinimalWidthLeft

View File

@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/power_saving.h"
#include "base/random.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
namespace Ui {
namespace {

View File

@ -12,7 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/text/text_options.h"
#include "ui/painter.h"
#include "lang/lang_keys.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_window.h" // st::columnMinimalWidthLeft
namespace Ui {

View File

@ -12,7 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/wrap/padding_wrap.h"
#include "ui/wrap/slide_wrap.h"
#include "lang/lang_keys.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_info.h"
namespace Ui {

View File

@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_channel.h"
#include "lang/lang_keys.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
namespace Ui {
namespace {

View File

@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/painter.h"
#include "lang/lang_keys.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_menu_icons.h"
namespace Lang {
@ -69,16 +70,9 @@ StringWithReacted ReplaceTag<StringWithReacted>::Call(
namespace Ui {
namespace {
using Text::CustomEmojiFactory;
constexpr auto kPreloaderAlpha = 0.2;
struct EntryData {
QString text;
QString date;
bool dateReacted = false;
QString customEntityData;
QImage userpic;
Fn<void()> callback;
};
using Text::CustomEmojiFactory;
class Action final : public Menu::ItemBase {
public:
@ -465,44 +459,11 @@ void Action::handleKeyPress(not_null<QKeyEvent*> e) {
} // namespace
class WhoReactedListMenu::EntryAction final : public Menu::ItemBase {
public:
EntryAction(
not_null<RpWidget*> parent,
CustomEmojiFactory factory,
const style::Menu &st,
EntryData &&data);
void setData(EntryData &&data);
not_null<QAction*> action() const override;
bool isEnabled() const override;
private:
int contentHeight() const override;
void paint(Painter &&p);
const not_null<QAction*> _dummyAction;
const CustomEmojiFactory _customEmojiFactory;
const style::Menu &_st;
const int _height = 0;
Text::String _text;
Text::String _date;
std::unique_ptr<Ui::Text::CustomEmoji> _custom;
QImage _userpic;
int _textWidth = 0;
int _customSize = 0;
bool _dateReacted = false;
};
WhoReactedListMenu::EntryAction::EntryAction(
WhoReactedEntryAction::WhoReactedEntryAction(
not_null<RpWidget*> parent,
CustomEmojiFactory customEmojiFactory,
const style::Menu &st,
EntryData &&data)
Data &&data)
: ItemBase(parent, st)
, _dummyAction(CreateChild<QAction>(parent.get()))
, _customEmojiFactory(std::move(customEmojiFactory))
@ -521,19 +482,19 @@ WhoReactedListMenu::EntryAction::EntryAction(
enableMouseSelecting();
}
not_null<QAction*> WhoReactedListMenu::EntryAction::action() const {
not_null<QAction*> WhoReactedEntryAction::action() const {
return _dummyAction.get();
}
bool WhoReactedListMenu::EntryAction::isEnabled() const {
bool WhoReactedEntryAction::isEnabled() const {
return true;
}
int WhoReactedListMenu::EntryAction::contentHeight() const {
int WhoReactedEntryAction::contentHeight() const {
return _height;
}
void WhoReactedListMenu::EntryAction::setData(EntryData &&data) {
void WhoReactedEntryAction::setData(Data &&data) {
setClickedCallback(std::move(data.callback));
_userpic = std::move(data.userpic);
_text.setMarkedText(_st.itemStyle, { data.text }, MenuTextOptions);
@ -546,7 +507,10 @@ void WhoReactedListMenu::EntryAction::setData(EntryData &&data) {
MenuTextOptions);
}
_dateReacted = data.dateReacted;
_custom = _customEmojiFactory(data.customEntityData, [=] { update(); });
_preloader = data.preloader;
_custom = _customEmojiFactory
? _customEmojiFactory(data.customEntityData, [=] { update(); })
: nullptr;
const auto ratio = style::DevicePixelRatio();
const auto size = Emoji::GetSizeNormal() / ratio;
_customSize = Text::AdjustCustomEmojiSize(size);
@ -565,7 +529,7 @@ void WhoReactedListMenu::EntryAction::setData(EntryData &&data) {
update();
}
void WhoReactedListMenu::EntryAction::paint(Painter &&p) {
void WhoReactedEntryAction::paint(Painter &&p) {
const auto enabled = isEnabled();
const auto selected = isSelected();
if (selected && _st.itemBgOver->c.alpha() < 255) {
@ -578,7 +542,18 @@ void WhoReactedListMenu::EntryAction::paint(Painter &&p) {
const auto photoSize = st::defaultWhoRead.photoSize;
const auto photoLeft = st::defaultWhoRead.photoLeft;
const auto photoTop = (height() - photoSize) / 2;
if (!_userpic.isNull()) {
const auto preloaderBrush = _preloader
? [&] {
auto color = _st.itemFg->c;
color.setAlphaF(color.alphaF() * kPreloaderAlpha);
return QBrush(color);
}() : QBrush();
if (_preloader) {
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(preloaderBrush);
p.drawEllipse(photoLeft, photoTop, photoSize, photoSize);
} else if (!_userpic.isNull()) {
p.drawImage(photoLeft, photoTop, _userpic);
} else if (!_custom) {
st::menuIconReactions.paintInCenter(
@ -590,17 +565,31 @@ void WhoReactedListMenu::EntryAction::paint(Painter &&p) {
const auto textTop = withDate
? st::whoReadNameWithDateTop
: (height() - _st.itemStyle.font->height) / 2;
p.setPen(selected
? _st.itemFgOver
: enabled
? _st.itemFg
: _st.itemFgDisabled);
_text.drawLeftElided(
p,
st::defaultWhoRead.nameLeft,
textTop,
_textWidth,
width());
if (_preloader) {
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(preloaderBrush);
const auto height = _st.itemStyle.font->height / 2;
p.drawRoundedRect(
st::defaultWhoRead.nameLeft,
textTop + (_st.itemStyle.font->height - height) / 2,
_textWidth,
height,
height / 2.,
height / 2.);
} else {
p.setPen(selected
? _st.itemFgOver
: enabled
? _st.itemFg
: _st.itemFgDisabled);
_text.drawLeftElided(
p,
st::defaultWhoRead.nameLeft,
textTop,
_textWidth,
width());
}
if (withDate) {
const auto iconPosition = QPoint(
st::defaultWhoRead.nameLeft,
@ -690,11 +679,11 @@ void WhoReactedListMenu::populate(
addedToBottom = 0;
}
auto index = 0;
const auto append = [&](EntryData &&data) {
const auto append = [&](WhoReactedEntryData &&data) {
if (index < _actions.size()) {
_actions[index]->setData(std::move(data));
} else {
auto item = base::make_unique_q<EntryAction>(
auto item = base::make_unique_q<WhoReactedEntryAction>(
menu->menu(),
_customEmojiFactory,
menu->menu()->st(),

View File

@ -9,11 +9,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/unique_qptr.h"
#include "ui/text/text_block.h"
#include "ui/widgets/menu/menu_item_base.h"
namespace Ui {
namespace Menu {
class ItemBase;
} // namespace Menu
class PopupMenu;
@ -56,6 +54,52 @@ struct WhoReadContent {
Fn<void(uint64)> participantChosen,
Fn<void()> showAllChosen);
struct WhoReactedEntryData {
QString text;
QString date;
bool dateReacted = false;
bool preloader = false;
QString customEntityData;
QImage userpic;
Fn<void()> callback;
};
class WhoReactedEntryAction final : public Menu::ItemBase {
public:
using Data = WhoReactedEntryData;
WhoReactedEntryAction(
not_null<RpWidget*> parent,
Text::CustomEmojiFactory factory,
const style::Menu &st,
Data &&data);
void setData(Data &&data);
not_null<QAction*> action() const override;
bool isEnabled() const override;
private:
int contentHeight() const override;
void paint(Painter &&p);
const not_null<QAction*> _dummyAction;
const Text::CustomEmojiFactory _customEmojiFactory;
const style::Menu &_st;
const int _height = 0;
Text::String _text;
Text::String _date;
std::unique_ptr<Ui::Text::CustomEmoji> _custom;
QImage _userpic;
int _textWidth = 0;
int _customSize = 0;
bool _dateReacted = false;
bool _preloader = false;
};
class WhoReactedListMenu final {
public:
WhoReactedListMenu(
@ -72,13 +116,11 @@ public:
Fn<void()> appendBottomActions = nullptr);
private:
class EntryAction;
const Text::CustomEmojiFactory _customEmojiFactory;
const Fn<void(uint64)> _participantChosen;
const Fn<void()> _showAllChosen;
std::vector<not_null<EntryAction*>> _actions;
std::vector<not_null<WhoReactedEntryAction*>> _actions;
};