init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,477 @@
/*
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 "info/bot/earn/info_bot_earn_list.h"
#include "api/api_credits.h"
#include "api/api_filter_updates.h"
#include "base/unixtime.h"
#include "core/ui_integration.h"
#include "data/data_channel_earn.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "data/stickers/data_custom_emoji.h"
#include "info/bot/earn/info_bot_earn_widget.h"
#include "info/bot/starref/info_bot_starref_common.h"
#include "info/bot/starref/info_bot_starref_join_widget.h"
#include "info/channel_statistics/earn/earn_format.h"
#include "info/info_controller.h"
#include "info/info_memento.h"
#include "info/statistics/info_statistics_inner_widget.h" // FillLoading.
#include "info/statistics/info_statistics_list_controllers.h"
#include "lang/lang_keys.h"
#include "main/main_account.h"
#include "main/main_session.h"
#include "settings/settings_credits_graphics.h"
#include "statistics/chart_widget.h"
#include "statistics/widgets/chart_header_widget.h"
#include "ui/effects/credits_graphics.h"
#include "ui/layers/generic_box.h"
#include "ui/rect.h"
#include "ui/toast/toast.h"
#include "ui/vertical_list.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/slider_natural_width.h"
#include "ui/wrap/slide_wrap.h"
#include "styles/style_boxes.h"
#include "styles/style_channel_earn.h"
#include "styles/style_credits.h"
#include "styles/style_settings.h"
#include "styles/style_statistics.h"
namespace Info::BotEarn {
namespace {
void AddHeader(
not_null<Ui::VerticalLayout*> content,
tr::phrase<> text) {
Ui::AddSkip(content);
const auto header = content->add(
object_ptr<Ui::FlatLabel>(
content,
text(),
st::channelEarnHeaderLabel),
st::boxRowPadding);
header->resizeToWidth(header->width());
}
} // namespace
InnerWidget::InnerWidget(QWidget *parent, not_null<Controller*> controller)
: VerticalLayout(parent)
, _controller(controller)
, _show(controller->uiShow()) {
}
void InnerWidget::load() {
const auto apiLifetime = lifetime().make_state<rpl::lifetime>();
const auto request = [=](Fn<void(Data::CreditsEarnStatistics)> done) {
const auto api = apiLifetime->make_state<Api::CreditsEarnStatistics>(
peer()->asUser());
api->request(
) | rpl::on_error_done([show = _show](const QString &error) {
show->showToast(error);
}, [=] {
done(api->data());
apiLifetime->destroy();
}, *apiLifetime);
};
Info::Statistics::FillLoading(
this,
Info::Statistics::LoadingType::Earn,
_loaded.events_starting_with(false) | rpl::map(!rpl::mappers::_1),
_showFinished.events());
_showFinished.events(
) | rpl::take(1) | rpl::on_next([=, this, peer = peer()] {
request([=](Data::CreditsEarnStatistics state) {
_state = state;
_loaded.fire(true);
fill();
peer->session().account().mtpUpdates(
) | rpl::on_next([=](const MTPUpdates &updates) {
using TL = MTPDupdateStarsRevenueStatus;
Api::PerformForUpdate<TL>(updates, [&](const TL &d) {
const auto peerId = peerFromMTP(d.vpeer());
if (peerId == peer->id) {
request([=](Data::CreditsEarnStatistics state) {
_state = state;
_stateUpdated.fire({});
});
}
});
}, lifetime());
});
}, lifetime());
}
void InnerWidget::fill() {
using namespace Info::ChannelEarn;
const auto container = this;
const auto &data = _state;
const auto multiplier = data.usdRate;
constexpr auto kMinorLength = 3;
auto availableBalanceValue = rpl::single(
data.availableBalance
) | rpl::then(
_stateUpdated.events() | rpl::map([=] {
return _state.availableBalance;
})
);
auto overallBalanceValue = rpl::single(
data.overallRevenue
) | rpl::then(
_stateUpdated.events() | rpl::map([=] {
return _state.overallRevenue;
})
);
auto valueToString = [](CreditsAmount v) {
return Lang::FormatCreditsAmountDecimal(v);
};
if (data.revenueGraph.chart) {
Ui::AddSkip(container);
Ui::AddSkip(container);
using Type = Statistic::ChartViewType;
const auto widget = container->add(
object_ptr<Statistic::ChartWidget>(container),
st::statisticsLayerMargins);
auto chart = data.revenueGraph.chart;
chart.currencyRate = data.usdRate;
widget->setChartData(chart, Type::StackBar);
widget->setTitle(tr::lng_bot_earn_chart_revenue());
Ui::AddSkip(container);
Ui::AddDivider(container);
Ui::AddSkip(container);
Statistic::FixCacheForHighDPIChartWidget(container);
}
{
AddHeader(container, tr::lng_bot_earn_overview_title);
Ui::AddSkip(container, st::channelEarnOverviewTitleSkip);
const auto addOverview = [&](
rpl::producer<CreditsAmount> value,
const tr::phrase<> &text) {
const auto line = container->add(
Ui::CreateSkipWidget(container, 0),
st::boxRowPadding);
const auto majorLabel = Ui::CreateChild<Ui::FlatLabel>(
line,
rpl::duplicate(value) | rpl::map(valueToString),
st::channelEarnOverviewMajorLabel);
const auto icon = Ui::CreateSingleStarWidget(
line,
majorLabel->height());
const auto secondMinorLabel = Ui::CreateChild<Ui::FlatLabel>(
line,
std::move(
value
) | rpl::map([=](CreditsAmount v) {
return v
? ToUsd(v, multiplier, kMinorLength)
: QString();
}),
st::channelEarnOverviewSubMinorLabel);
rpl::combine(
line->widthValue(),
majorLabel->sizeValue()
) | rpl::on_next([=](int available, const QSize &size) {
line->resize(line->width(), size.height());
majorLabel->moveToLeft(
icon->width() + st::channelEarnOverviewMinorLabelSkip,
majorLabel->y());
secondMinorLabel->resizeToWidth(available
- size.width()
- icon->width());
secondMinorLabel->moveToLeft(
rect::right(majorLabel)
+ st::channelEarnOverviewSubMinorLabelPos.x(),
st::channelEarnOverviewSubMinorLabelPos.y());
}, majorLabel->lifetime());
Ui::ToggleChildrenVisibility(line, true);
Ui::AddSkip(container);
container->add(
object_ptr<Ui::FlatLabel>(
container,
text(),
st::channelEarnOverviewSubMinorLabel),
st::boxRowPadding);
};
addOverview(
rpl::duplicate(availableBalanceValue),
tr::lng_bot_earn_available);
Ui::AddSkip(container);
Ui::AddSkip(container);
addOverview(
rpl::single(data.currentBalance),
tr::lng_bot_earn_reward);
Ui::AddSkip(container);
Ui::AddSkip(container);
addOverview(
rpl::duplicate(overallBalanceValue),
tr::lng_bot_earn_total);
Ui::AddSkip(container);
Ui::AddSkip(container);
Ui::AddDividerText(container, tr::lng_bot_earn_balance_about());
Ui::AddSkip(container);
}
{
AddHeader(container, tr::lng_bot_earn_balance_title);
Ui::AddSkip(container);
auto dateValue = rpl::single(
data.nextWithdrawalAt
) | rpl::then(
_stateUpdated.events() | rpl::map([=] {
return _state.nextWithdrawalAt;
})
);
::Settings::AddWithdrawalWidget(
container,
_controller->parentController(),
peer(),
rpl::single(
data.buyAdsUrl
) | rpl::then(
_stateUpdated.events() | rpl::map([=] {
return _state.buyAdsUrl;
})
),
rpl::duplicate(availableBalanceValue),
rpl::duplicate(dateValue),
_state.isWithdrawalEnabled,
rpl::duplicate(
availableBalanceValue
) | rpl::map([=](CreditsAmount v) {
return v ? ToUsd(v, multiplier, kMinorLength) : QString();
}));
container->resizeToWidth(container->width());
}
if (BotStarRef::Join::Allowed(peer()) && !peer()->isSelf()) {
const auto button = BotStarRef::AddViewListButton(
container,
tr::lng_credits_summary_earn_title(),
tr::lng_credits_summary_earn_about(),
true);
button->setClickedCallback([=] {
_controller->showSection(BotStarRef::Join::Make(peer()));
});
Ui::AddSkip(container);
Ui::AddDivider(container);
}
if (!peer()->isSelf()) {
fillHistory();
}
}
void InnerWidget::fillHistory() {
const auto container = this;
Ui::AddSkip(container, st::settingsPremiumOptionsPadding.top());
const auto history = container->add(
object_ptr<Ui::VerticalLayout>(container));
const auto sectionIndex = history->lifetime().make_state<int>(0);
const auto fill = [=, peer = peer()](
not_null<PeerData*> premiumBot,
const Data::CreditsStatusSlice &fullSlice,
const Data::CreditsStatusSlice &inSlice,
const Data::CreditsStatusSlice &outSlice) {
if (fullSlice.list.empty()) {
return;
}
const auto inner = history->add(
object_ptr<Ui::VerticalLayout>(history));
const auto hasOneTab = inSlice.list.empty() && outSlice.list.empty();
const auto hasIn = !inSlice.list.empty();
const auto hasOut = !outSlice.list.empty();
const auto fullTabText = tr::lng_credits_summary_history_tab_full(
tr::now);
const auto inTabText = tr::lng_credits_summary_history_tab_in(
tr::now);
const auto outTabText = tr::lng_credits_summary_history_tab_out(
tr::now);
if (hasOneTab) {
const auto header = inner->add(
object_ptr<Statistic::Header>(inner),
st::statisticsLayerMargins
+ st::boostsChartHeaderPadding);
header->resizeToWidth(header->width());
header->setTitle(fullTabText);
header->setSubTitle({});
}
const auto slider = inner->add(
object_ptr<Ui::SlideWrap<Ui::CustomWidthSlider>>(
inner,
object_ptr<Ui::CustomWidthSlider>(
inner,
st::defaultTabsSlider)),
st::boxRowPadding);
slider->toggle(!hasOneTab, anim::type::instant);
slider->entity()->addSection(fullTabText);
if (hasIn) {
slider->entity()->addSection(inTabText);
}
if (hasOut) {
slider->entity()->addSection(outTabText);
}
slider->entity()->setActiveSectionFast(*sectionIndex);
{
const auto &st = st::defaultTabsSlider;
slider->entity()->setNaturalWidth(0
+ st.labelStyle.font->width(fullTabText)
+ (hasIn ? st.labelStyle.font->width(inTabText) : 0)
+ (hasOut ? st.labelStyle.font->width(outTabText) : 0)
+ rect::m::sum::h(st::boxRowPadding));
}
const auto fullWrap = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
const auto inWrap = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
const auto outWrap = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
rpl::single(slider->entity()->activeSection()) | rpl::then(
slider->entity()->sectionActivated()
) | rpl::on_next([=](int index) {
if (index == 0) {
fullWrap->toggle(true, anim::type::instant);
inWrap->toggle(false, anim::type::instant);
outWrap->toggle(false, anim::type::instant);
} else if (index == 1) {
inWrap->toggle(true, anim::type::instant);
fullWrap->toggle(false, anim::type::instant);
outWrap->toggle(false, anim::type::instant);
} else {
outWrap->toggle(true, anim::type::instant);
fullWrap->toggle(false, anim::type::instant);
inWrap->toggle(false, anim::type::instant);
}
*sectionIndex = index;
}, inner->lifetime());
const auto controller = _controller->parentController();
const auto entryClicked = [=](
const Data::CreditsHistoryEntry &e,
const Data::SubscriptionEntry &s) {
controller->uiShow()->show(Box(
::Settings::ReceiptCreditsBox,
controller,
e,
s));
};
Info::Statistics::AddCreditsHistoryList(
controller->uiShow(),
fullSlice,
fullWrap->entity(),
entryClicked,
peer,
true,
true);
Info::Statistics::AddCreditsHistoryList(
controller->uiShow(),
inSlice,
inWrap->entity(),
entryClicked,
peer,
true,
false);
Info::Statistics::AddCreditsHistoryList(
controller->uiShow(),
outSlice,
outWrap->entity(),
std::move(entryClicked),
peer,
false,
true);
Ui::AddSkip(inner);
Ui::AddSkip(inner);
};
const auto apiLifetime = history->lifetime().make_state<rpl::lifetime>();
rpl::single(rpl::empty) | rpl::then(
_stateUpdated.events()
) | rpl::on_next([=, peer = peer()] {
using Api = Api::CreditsHistory;
const auto apiFull = apiLifetime->make_state<Api>(peer, true, true);
const auto apiIn = apiLifetime->make_state<Api>(peer, true, false);
const auto apiOut = apiLifetime->make_state<Api>(peer, false, true);
apiFull->request({}, [=](Data::CreditsStatusSlice fullSlice) {
apiIn->request({}, [=](Data::CreditsStatusSlice inSlice) {
apiOut->request({}, [=](Data::CreditsStatusSlice outSlice) {
::Api::PremiumPeerBot(
&_controller->session()
) | rpl::on_next([=](not_null<PeerData*> bot) {
fill(bot, fullSlice, inSlice, outSlice);
container->resizeToWidth(container->width());
while (history->count() > 1) {
delete history->widgetAt(0);
}
apiLifetime->destroy();
}, *apiLifetime);
});
});
});
}, history->lifetime());
}
void InnerWidget::saveState(not_null<Memento*> memento) {
memento->setState(base::take(_state));
}
void InnerWidget::restoreState(not_null<Memento*> memento) {
_state = memento->state();
if (_state) {
fill();
} else {
load();
}
Ui::RpWidget::resizeToWidth(width());
}
rpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {
return _scrollToRequests.events();
}
auto InnerWidget::showRequests() const -> rpl::producer<ShowRequest> {
return _showRequests.events();
}
void InnerWidget::showFinished() {
_showFinished.fire({});
}
void InnerWidget::setInnerFocus() {
_focusRequested.fire({});
}
not_null<PeerData*> InnerWidget::peer() const {
return _controller->statisticsTag().peer;
}
} // namespace Info::BotEarn

View File

@@ -0,0 +1,67 @@
/*
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 "data/data_credits_earn.h"
#include "ui/widgets/scroll_area.h"
#include "ui/wrap/vertical_layout.h"
namespace Ui {
class Show;
} // namespace Ui
namespace Info {
class Controller;
} // namespace Info
namespace Info::BotEarn {
class Memento;
[[nodiscard]] QImage IconCurrency(
const style::FlatLabel &label,
const QColor &c);
class InnerWidget final : public Ui::VerticalLayout {
public:
struct ShowRequest final {
};
InnerWidget(QWidget *parent, not_null<Controller*> controller);
[[nodiscard]] not_null<PeerData*> peer() const;
[[nodiscard]] rpl::producer<Ui::ScrollToRequest> scrollToRequests() const;
[[nodiscard]] rpl::producer<ShowRequest> showRequests() const;
void showFinished();
void setInnerFocus();
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
private:
void load();
void fill();
void fillHistory();
not_null<Controller*> _controller;
std::shared_ptr<Ui::Show> _show;
Data::CreditsEarnStatistics _state;
rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
rpl::event_stream<ShowRequest> _showRequests;
rpl::event_stream<> _showFinished;
rpl::event_stream<> _focusRequested;
rpl::event_stream<bool> _loaded;
rpl::event_stream<> _stateUpdated;
};
} // namespace Info::BotEarn

View File

@@ -0,0 +1,118 @@
/*
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 "info/bot/earn/info_bot_earn_widget.h"
#include "info/bot/earn/info_bot_earn_list.h"
#include "info/info_controller.h"
#include "info/info_memento.h"
#include "lang/lang_keys.h"
#include "ui/ui_utility.h"
namespace Info::BotEarn {
Memento::Memento(not_null<Controller*> controller)
: ContentMemento(controller->statisticsTag()) {
}
Memento::Memento(not_null<PeerData*> peer)
: ContentMemento(Info::Statistics::Tag{ peer, {}, {} }) {
}
Memento::~Memento() = default;
Section Memento::section() const {
return Section(Section::Type::BotEarn);
}
void Memento::setState(SavedState state) {
_state = std::move(state);
}
Memento::SavedState Memento::state() {
return base::take(_state);
}
object_ptr<ContentWidget> Memento::createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) {
auto result = object_ptr<Widget>(parent, controller);
result->setInternalState(geometry, this);
return result;
}
Widget::Widget(
QWidget *parent,
not_null<Controller*> controller)
: ContentWidget(parent, controller)
, _inner(setInnerWidget(object_ptr<InnerWidget>(this, controller))) {
_inner->showRequests(
) | rpl::on_next([=](InnerWidget::ShowRequest request) {
}, _inner->lifetime());
_inner->scrollToRequests(
) | rpl::on_next([=](const Ui::ScrollToRequest &request) {
scrollTo(request);
}, _inner->lifetime());
}
not_null<PeerData*> Widget::peer() const {
return _inner->peer();
}
bool Widget::showInternal(not_null<ContentMemento*> memento) {
return (memento->statisticsTag().peer == peer());
}
rpl::producer<QString> Widget::title() {
return tr::lng_bot_earn_title();
}
void Widget::setInternalState(
const QRect &geometry,
not_null<Memento*> memento) {
setGeometry(geometry);
Ui::SendPendingMoveResizeEvents(this);
restoreState(memento);
}
rpl::producer<bool> Widget::desiredShadowVisibility() const {
return rpl::single<bool>(true);
}
void Widget::showFinished() {
_inner->showFinished();
}
void Widget::setInnerFocus() {
_inner->setInnerFocus();
}
std::shared_ptr<ContentMemento> Widget::doCreateMemento() {
auto result = std::make_shared<Memento>(controller());
saveState(result.get());
return result;
}
void Widget::saveState(not_null<Memento*> memento) {
memento->setScrollTop(scrollTopSave());
_inner->saveState(memento);
}
void Widget::restoreState(not_null<Memento*> memento) {
_inner->restoreState(memento);
scrollTopRestore(memento->scrollTop());
}
std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer) {
return std::make_shared<Info::Memento>(
std::vector<std::shared_ptr<ContentMemento>>(
1,
std::make_shared<Memento>(peer)));
}
} // namespace Info::BotEarn

View File

@@ -0,0 +1,68 @@
/*
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 "data/data_credits_earn.h"
#include "info/info_content_widget.h"
namespace Info::BotEarn {
class InnerWidget;
class Memento final : public ContentMemento {
public:
Memento(not_null<Controller*> controller);
Memento(not_null<PeerData*> peer);
~Memento();
object_ptr<ContentWidget> createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) override;
Section section() const override;
using SavedState = Data::CreditsEarnStatistics;
void setState(SavedState states);
[[nodiscard]] SavedState state();
private:
SavedState _state;
};
class Widget final : public ContentWidget {
public:
Widget(QWidget *parent, not_null<Controller*> controller);
bool showInternal(not_null<ContentMemento*> memento) override;
rpl::producer<QString> title() override;
rpl::producer<bool> desiredShadowVisibility() const override;
void showFinished() override;
void setInnerFocus() override;
[[nodiscard]] not_null<PeerData*> peer() const;
void setInternalState(
const QRect &geometry,
not_null<Memento*> memento);
private:
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
std::shared_ptr<ContentMemento> doCreateMemento() override;
const not_null<InnerWidget*> _inner;
};
[[nodiscard]] std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer);
} // namespace Info::BotEarn

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,112 @@
/*
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 "base/object_ptr.h"
#include "data/data_user.h"
namespace Ui {
class AbstractButton;
class RoundButton;
class VerticalLayout;
class BoxContent;
class RpWidget;
class Show;
} // namespace Ui
namespace style {
struct RoundButton;
struct InputField;
} // namespace style
namespace Main {
class Session;
} // namespace Main
namespace Info::BotStarRef {
struct ConnectedBotState {
StarRefProgram program;
QString link;
TimeId date = 0;
int users = 0;
bool unresolved = false;
bool revoked = false;
};
struct ConnectedBot {
not_null<UserData*> bot;
ConnectedBotState state;
};
using ConnectedBots = std::vector<ConnectedBot>;
[[nodiscard]] QString FormatCommission(ushort commission);
[[nodiscard]] QString FormatProgramDuration(int durationMonths);
[[nodiscard]] rpl::producer<TextWithEntities> FormatForProgramDuration(
int durationMonths);
[[nodiscard]] not_null<Ui::AbstractButton*> AddViewListButton(
not_null<Ui::VerticalLayout*> parent,
rpl::producer<QString> title,
rpl::producer<QString> subtitle,
bool newBadge = false);
void AddFullWidthButtonFooter(
not_null<Ui::BoxContent*> box,
not_null<Ui::RpWidget*> button,
rpl::producer<TextWithEntities> text);
[[nodiscard]] object_ptr<Ui::BoxContent> StarRefLinkBox(
ConnectedBot row,
not_null<PeerData*> peer);
[[nodiscard]] object_ptr<Ui::BoxContent> JoinStarRefBox(
ConnectedBot row,
not_null<PeerData*> initialRecipient,
std::vector<not_null<PeerData*>> recipients,
Fn<void(ConnectedBotState)> done = nullptr);
[[nodiscard]] object_ptr<Ui::BoxContent> ConfirmEndBox(Fn<void()> finish);
void ResolveRecipients(
not_null<Main::Session*> session,
Fn<void(std::vector<not_null<PeerData*>>)> done);
std::unique_ptr<Ui::AbstractButton> MakePeerBubbleButton(
not_null<QWidget*> parent,
not_null<PeerData*> peer,
Ui::RpWidget *right = nullptr,
const style::color *bgOverride = nullptr);
void ConfirmUpdate(
std::shared_ptr<Ui::Show> show,
not_null<UserData*> bot,
const StarRefProgram &program,
bool exists,
Fn<void(Fn<void(bool)> done)> update);
void UpdateProgram(
std::shared_ptr<Ui::Show> show,
not_null<UserData*> bot,
const StarRefProgram &program,
Fn<void(bool)> done);
void FinishProgram(
std::shared_ptr<Ui::Show> show,
not_null<UserData*> bot,
Fn<void(bool)> done);
[[nodiscard]] ConnectedBots Parse(
not_null<Main::Session*> session,
const MTPpayments_ConnectedStarRefBots &bots);
[[nodiscard]] object_ptr<Ui::AbstractButton> MakeLinkLabel(
not_null<QWidget*> parent,
const QString &link,
const style::InputField *stOverride = nullptr);
[[nodiscard]] object_ptr<Ui::RpWidget> CreateLinkHeaderIcon(
not_null<QWidget*> parent,
not_null<Main::Session*> session,
int usersCount = 0);
} // namespace Info::BotStarRef

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,89 @@
/*
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 "info/info_content_widget.h"
namespace Ui::Premium {
class TopBarAbstract;
} // namespace Ui::Premium
namespace Ui {
template <typename Widget>
class FadeWrap;
class IconButton;
class AbstractButton;
class VerticalLayout;
class BoxContent;
} // namespace Ui
namespace Window {
class SessionController;
} // namespace Window
namespace Info::BotStarRef::Join {
class InnerWidget;
class Memento final : public ContentMemento {
public:
Memento(not_null<Controller*> controller);
Memento(not_null<PeerData*> peer);
~Memento();
object_ptr<ContentWidget> createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) override;
Section section() const override;
};
class Widget final : public ContentWidget {
public:
Widget(QWidget *parent, not_null<Controller*> controller);
bool showInternal(not_null<ContentMemento*> memento) override;
rpl::producer<QString> title() override;
rpl::producer<bool> desiredShadowVisibility() const override;
void showFinished() override;
void setInnerFocus() override;
void enableBackButton() override;
[[nodiscard]] not_null<PeerData*> peer() const;
void setInternalState(
const QRect &geometry,
not_null<Memento*> memento);
private:
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
[[nodiscard]] std::unique_ptr<Ui::Premium::TopBarAbstract> setupTop();
std::shared_ptr<ContentMemento> doCreateMemento() override;
const not_null<InnerWidget*> _inner;
std::unique_ptr<Ui::Premium::TopBarAbstract> _top;
base::unique_qptr<Ui::FadeWrap<Ui::IconButton>> _back;
base::unique_qptr<Ui::IconButton> _close;
rpl::variable<bool> _backEnabled;
};
[[nodiscard]] bool Allowed(not_null<PeerData*> peer);
[[nodiscard]] std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer);
[[nodiscard]] object_ptr<Ui::BoxContent> ProgramsListBox(
not_null<Window::SessionController*> window,
not_null<PeerData*> peer);
} // namespace Info::BotStarRef::Join

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,86 @@
/*
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 "info/info_content_widget.h"
#include "info/bot/starref/info_bot_starref_common.h"
namespace Ui::Premium {
class TopBarAbstract;
} // namespace Ui::Premium
namespace Ui {
template <typename Widget>
class FadeWrap;
class IconButton;
class AbstractButton;
class VerticalLayout;
} // namespace Ui
namespace Info::BotStarRef::Setup {
struct State;
class InnerWidget;
class Memento final : public ContentMemento {
public:
Memento(not_null<Controller*> controller);
Memento(not_null<PeerData*> peer);
~Memento();
object_ptr<ContentWidget> createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) override;
Section section() const override;
};
class Widget final : public ContentWidget {
public:
Widget(QWidget *parent, not_null<Controller*> controller);
bool showInternal(not_null<ContentMemento*> memento) override;
rpl::producer<QString> title() override;
rpl::producer<bool> desiredShadowVisibility() const override;
void showFinished() override;
void setInnerFocus() override;
void enableBackButton() override;
[[nodiscard]] not_null<PeerData*> peer() const;
void setInternalState(
const QRect &geometry,
not_null<Memento*> memento);
private:
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
[[nodiscard]] std::unique_ptr<Ui::Premium::TopBarAbstract> setupTop();
[[nodiscard]] std::unique_ptr<Ui::RpWidget> setupBottom();
std::shared_ptr<ContentMemento> doCreateMemento() override;
const not_null<InnerWidget*> _inner;
const not_null<State*> _state;
std::unique_ptr<Ui::Premium::TopBarAbstract> _top;
base::unique_qptr<Ui::FadeWrap<Ui::IconButton>> _back;
base::unique_qptr<Ui::IconButton> _close;
rpl::variable<bool> _backEnabled;
std::unique_ptr<Ui::RpWidget> _bottom;
};
[[nodiscard]] bool Allowed(not_null<PeerData*> peer);
[[nodiscard]] std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer);
} // namespace Info::BotStarRef::Setup

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
/*
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
class PeerData;
namespace Data {
struct BoostPrepaidGiveaway;
} // namespace Data
namespace Window {
class SessionNavigation;
} // namespace Window
namespace Ui {
class GenericBox;
} // namespace Ui
void CreateGiveawayBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
Fn<void()> reloadOnDone,
std::optional<Data::BoostPrepaidGiveaway> prepaidGiveaway);

View File

@@ -0,0 +1,190 @@
/*
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 "info/channel_statistics/boosts/giveaway/boost_badge.h"
#include "ui/effects/radial_animation.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/rp_widget.h"
#include "ui/widgets/labels.h"
#include "styles/style_giveaway.h"
#include "styles/style_statistics.h"
#include "styles/style_widgets.h"
namespace Info::Statistics {
not_null<Ui::RpWidget*> InfiniteRadialAnimationWidget(
not_null<Ui::RpWidget*> parent,
int size,
const style::InfiniteRadialAnimation *st) {
class Widget final : public Ui::RpWidget {
public:
Widget(
not_null<Ui::RpWidget*> p,
int size,
const style::InfiniteRadialAnimation *st)
: Ui::RpWidget(p)
, _st(st ? st : &st::startGiveawayButtonLoading)
, _animation([=] { update(); }, *_st) {
resize(size, size);
shownValue() | rpl::on_next([=](bool v) {
return v
? _animation.start()
: _animation.stop(anim::type::instant);
}, lifetime());
}
protected:
void paintEvent(QPaintEvent *e) override {
auto p = QPainter(this);
p.setPen(st::activeButtonFg);
p.setBrush(st::activeButtonFg);
const auto r = rect() - Margins(_st->thickness);
_animation.draw(p, r.topLeft(), r.size(), width());
}
private:
const style::InfiniteRadialAnimation *_st;
Ui::InfiniteRadialAnimation _animation;
};
return Ui::CreateChild<Widget>(parent.get(), size, st);
}
void AddChildToWidgetCenter(
not_null<Ui::RpWidget*> parent,
not_null<Ui::RpWidget*> child) {
parent->sizeValue(
) | rpl::on_next([=](const QSize &s) {
const auto size = child->size();
child->moveToLeft(
(s.width() - size.width()) / 2,
(s.height() - size.height()) / 2);
}, child->lifetime());
}
QImage CreateBadge(
const style::TextStyle &textStyle,
const QString &text,
int badgeHeight,
const style::margins &textPadding,
const style::color &bg,
const style::color &fg,
float64 bgOpacity,
const style::margins &iconPadding,
const style::icon &icon) {
auto badgeText = Ui::Text::String(textStyle, text);
const auto badgeTextWidth = badgeText.maxWidth();
const auto badgex = 0;
const auto badgey = 0;
const auto badgeh = 0 + badgeHeight;
const auto badgew = badgeTextWidth
+ rect::m::sum::h(textPadding);
auto result = QImage(
QSize(badgew, badgeh) * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
result.fill(Qt::transparent);
result.setDevicePixelRatio(style::DevicePixelRatio());
{
auto p = Painter(&result);
p.setPen(Qt::NoPen);
p.setBrush(bg);
const auto r = QRect(badgex, badgey, badgew, badgeh);
{
auto hq = PainterHighQualityEnabler(p);
auto o = ScopedPainterOpacity(p, bgOpacity);
p.drawRoundedRect(r, badgeh / 2, badgeh / 2);
}
p.setPen(fg);
p.setBrush(Qt::NoBrush);
badgeText.drawLeftElided(
p,
r.x() + textPadding.left(),
badgey + textPadding.top(),
badgew,
badgew * 2);
icon.paint(
p,
QPoint(r.x() + iconPadding.left(), r.y() + iconPadding.top()),
badgew * 2);
}
return result;
}
void AddLabelWithBadgeToButton(
not_null<Ui::RpWidget*> parent,
rpl::producer<QString> text,
rpl::producer<int> number,
rpl::producer<bool> shown) {
struct State {
QImage badge;
};
const auto state = parent->lifetime().make_state<State>();
const auto label = Ui::CreateChild<Ui::LabelSimple>(
parent.get(),
st::startGiveawayButtonLabelSimple);
std::move(
text
) | rpl::on_next([=](const QString &s) {
label->setText(s);
}, label->lifetime());
const auto count = Ui::CreateChild<Ui::RpWidget>(parent.get());
count->paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(count);
p.drawImage(0, 0, state->badge);
}, count->lifetime());
std::move(
number
) | rpl::on_next([=](int c) {
state->badge = Info::Statistics::CreateBadge(
st::startGiveawayButtonTextStyle,
QString::number(c),
st::boostsListBadgeHeight,
st::startGiveawayButtonBadgeTextPadding,
st::activeButtonFg,
st::activeButtonBg,
1.,
st::boostsListMiniIconPadding,
st::startGiveawayButtonMiniIcon);
count->resize(state->badge.size() / style::DevicePixelRatio());
count->update();
}, count->lifetime());
std::move(
shown
) | rpl::on_next([=](bool shown) {
count->setVisible(shown);
label->setVisible(shown);
}, count->lifetime());
rpl::combine(
parent->sizeValue(),
label->sizeValue(),
count->sizeValue()
) | rpl::on_next([=](
const QSize &s,
const QSize &s1,
const QSize &s2) {
const auto sum = st::startGiveawayButtonMiniIconSkip
+ s1.width()
+ s2.width();
const auto contentLeft = (s.width() - sum) / 2;
label->moveToLeft(contentLeft, (s.height() - s1.height()) / 2);
count->moveToLeft(
contentLeft + sum - s2.width(),
(s.height() - s2.height()) / 2 + st::boostsListMiniIconSkip);
}, parent->lifetime());
}
} // namespace Info::Statistics

View File

@@ -0,0 +1,47 @@
/*
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
namespace style {
struct InfiniteRadialAnimation;
struct TextStyle;
} // namespace style
namespace Ui {
class RpWidget;
} // namespace Ui
namespace Info::Statistics {
[[nodiscard]] QImage CreateBadge(
const style::TextStyle &textStyle,
const QString &text,
int badgeHeight,
const style::margins &textPadding,
const style::color &bg,
const style::color &fg,
float64 bgOpacity,
const style::margins &iconPadding,
const style::icon &icon);
[[nodiscard]] not_null<Ui::RpWidget*> InfiniteRadialAnimationWidget(
not_null<Ui::RpWidget*> parent,
int size,
const style::InfiniteRadialAnimation *st = nullptr);
void AddChildToWidgetCenter(
not_null<Ui::RpWidget*> parent,
not_null<Ui::RpWidget*> child);
void AddLabelWithBadgeToButton(
not_null<Ui::RpWidget*> parent,
rpl::producer<QString> text,
rpl::producer<int> number,
rpl::producer<bool> shown);
} // namespace Info::Statistics

View File

@@ -0,0 +1,285 @@
/*
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
*/
using "ui/basic.style";
using "boxes/boxes.style";
using "ui/effects/premium.style";
using "statistics/statistics.style";
giveawayTypeListItem: PeerListItem(defaultPeerListItem) {
height: 52px;
photoPosition: point(58px, 6px);
namePosition: point(110px, 8px);
statusPosition: point(110px, 28px);
photoSize: 42px;
}
giveawayUserpic: icon {{ "boosts/filled_gift", windowFgActive }};
giveawayUserpicSkip: 1px;
giveawayUserpicGroup: icon {{ "limits/groups", windowFgActive }};
giveawayRadioPosition: point(21px, 16px);
giveawayGiftCodeCountryButton: SettingsButton(reportReasonButton) {
}
giveawayGiftCodeCountrySelect: MultiSelect(defaultMultiSelect) {
}
giveawayGiftCodeChannelDeleteIcon: icon {{ "dialogs/dialogs_cancel_search", dialogsMenuIconFg }};
giveawayGiftCodeChannelDeleteIconOver: icon {{ "dialogs/dialogs_cancel_search", dialogsMenuIconFgOver }};
giveawayLoadingLabel: FlatLabel(membersAbout) {
}
giveawayGiftCodeTopHeight: 195px;
giveawayGiftCodeLink: FlatLabel(defaultFlatLabel) {
margin: margins(10px, 12px, 10px, 8px);
textFg: menuIconColor;
maxHeight: 24px;
}
giveawayGiftCodeLinkCopy: icon{{ "menu/copy", menuIconColor }};
giveawayGiftCodeLinkHeight: 42px;
giveawayGiftCodeLinkCopyWidth: 40px;
giveawayGiftCodeLinkMargin: margins(24px, 8px, 24px, 12px);
giveawayGiftCodeGiftOption: PremiumOption(premiumGiftOption) {
badgeShift: point(5px, 0px);
}
giveawayGiftCodeStartButton: RoundButton(defaultActiveButton) {
height: 42px;
textTop: 12px;
radius: 6px;
}
giveawayGiftCodeQuantitySubtitle: FlatLabel(defaultFlatLabel) {
style: TextStyle(semiboldTextStyle) {
font: font(boxFontSize semibold);
}
textFg: windowActiveTextFg;
minWidth: 240px;
align: align(right);
}
giveawayGiftCodeQuantityFloat: FlatLabel(defaultFlatLabel) {
style: semiboldTextStyle;
textFg: windowActiveTextFg;
minWidth: 50px;
align: align(center);
}
boostLinkStatsButton: IconButton(defaultIconButton) {
width: giveawayGiftCodeLinkCopyWidth;
height: giveawayGiftCodeLinkHeight;
icon: icon{{ "menu/stats", menuIconColor }};
iconOver: icon{{ "menu/stats", menuIconColor }};
ripple: emptyRippleAnimation;
}
giveawayGiftCodeTable: Table(defaultTable) {
labelMinWidth: 91px;
}
giveawayGiftCodeTableMargin: margins(24px, 4px, 24px, 4px);
giveawayGiftCodeLabelMargin: margins(13px, 10px, 13px, 10px);
giveawayGiftCodeValueMultiline: FlatLabel(defaultTableValue) {
minWidth: 128px;
maxHeight: 100px;
style: TextStyle(defaultTextStyle) {
font: font(10px);
linkUnderline: kLinkUnderlineNever;
}
}
giveawayGiftMessage: FlatLabel(defaultTableValue) {
minWidth: 128px;
maxHeight: 0px;
}
giveawayGiftMessageRemove: IconButton(defaultIconButton) {
width: 32px;
height: 32px;
icon: icon{{ "menu/delete", windowActiveTextFg }};
iconOver: icon{{ "menu/delete", windowActiveTextFg }};
rippleAreaPosition: point(0px, 0px);
rippleAreaSize: 32px;
ripple: RippleAnimation(defaultRippleAnimation) {
color: lightButtonBgRipple;
}
}
giveawayGiftCodeValueMargin: margins(13px, 9px, 13px, 9px);
giveawayGiftCodePeerMargin: margins(11px, 6px, 11px, 4px);
giveawayGiftCodeUserpic: UserpicButton(defaultUserpicButton) {
size: size(24px, 24px);
photoSize: 24px;
photoPosition: point(-1px, -1px);
}
giveawayGiftCodeNamePosition: point(32px, 4px);
giveawayGiftCodeCover: PremiumCover(userPremiumCover) {
starSize: size(92px, 90px);
starTopSkip: 20px;
titlePadding: margins(0px, 15px, 0px, 17px);
titleFont: font(15px semibold);
about: FlatLabel(userPremiumCoverAbout) {
textFg: windowBoldFg;
style: TextStyle(premiumAboutTextStyle) {
lineHeight: 17px;
}
}
}
giveawayGiftCodeCoverClosePosition: point(5px, 0px);
giveawayGiftCodeCoverDividerPadding: margins(0px, 11px, 0px, 5px);
giveawayGiftCodeTypeDividerPadding: margins(0px, 7px, 0px, 5px);
giveawayGiftCodeSliderPadding: margins(0px, 24px, 0px, 10px);
giveawayGiftCodeSliderFloatSkip: 6px;
giveawayGiftCodeChannelsSubsectionPadding: margins(0px, -1px, 0px, -4px);
giveawayGiftCodeAdditionalPaddingMin: margins(50px, 4px, 22px, 0px);
giveawayGiftCodeAdditionalField: InputField(defaultMultiSelectSearchField) {
}
giveawayGiftCodeAdditionalLabel: FlatLabel(defaultFlatLabel) {
style: semiboldTextStyle;
}
giveawayGiftCodeAdditionalLabelSkip: 12px;
giveawayGiftCodeChannelsPeerList: PeerList(boostsListBox) {
padding: margins(0px, 7px, 0px, 0px);
}
giveawayGiftCodeMembersPeerList: PeerList(defaultPeerList) {
item: PeerListItem(defaultPeerListItem) {
height: 50px;
namePosition: point(62px, 7px);
statusPosition: point(62px, 27px);
}
}
giveawayRadioMembersPosition: point(21px, 14px);
giveawayGiftCodeChannelsAddButton: SettingsButton(defaultSettingsButton) {
textFg: lightButtonFg;
textFgOver: lightButtonFgOver;
padding: margins(70px, 10px, 22px, 8px);
iconLeft: 28px;
}
giveawayGiftCodeChannelsDividerPadding: margins(0px, 5px, 0px, 5px);
giveawayGiftCodeFooter: FlatLabel(defaultFlatLabel) {
align: align(top);
textFg: windowBoldFg;
}
giveawayGiftCodeFooterMargin: margins(0px, 9px, 0px, 4px);
giveawayGiftCodeBoxButton: RoundButton(defaultActiveButton) {
height: 42px;
textTop: 12px;
style: semiboldTextStyle;
}
giveawayGiftCodeBox: Box(defaultBox) {
buttonPadding: margins(22px, 11px, 22px, 22px);
buttonHeight: 42px;
buttonWide: true;
button: giveawayGiftCodeBoxButton;
shadowIgnoreTopSkip: true;
}
giveawayGiftCodeBoxUpgradeNext: RoundButton(defaultLightButton, giveawayGiftCodeBoxButton) {
}
giveawayRefundedLabel: FlatLabel(boxLabel) {
align: align(top);
style: semiboldTextStyle;
textFg: attentionButtonFg;
}
giveawayRefundedPadding: margins(8px, 10px, 8px, 10px);
startGiveawayBox: Box(premiumGiftBox) {
shadowIgnoreTopSkip: true;
}
startGiveawayScrollArea: ScrollArea(boxScroll) {
deltax: 3px;
deltat: 50px;
}
startGiveawayBoxTitleClose: IconButton(boxTitleClose) {
ripple: universalRippleAnimation;
}
startGiveawayCover: PremiumCover(giveawayGiftCodeCover) {
bg: boxDividerBg;
additionalShadowForDarkThemes: false;
}
startGiveawayButtonLabelSimple: LabelSimple {
font: semiboldFont;
textFg: activeButtonFg;
}
startGiveawayButtonMiniIcon: icon{{ "boosts/boost_mini2", activeButtonBg }};
startGiveawayButtonMiniIconSkip: 5px;
startGiveawayButtonBadgeTextPadding: margins(16px, -1px, 6px, 0px);
startGiveawayButtonTextStyle: TextStyle(defaultTextStyle) {
font: semiboldFont;
}
startGiveawayButtonLoading: InfiniteRadialAnimation(defaultInfiniteRadialAnimation) {
color: activeButtonFg;
thickness: 2px;
}
starConvertButtonLoading: InfiniteRadialAnimation(startGiveawayButtonLoading) {
color: windowActiveTextFg;
thickness: 2px;
}
starGiftSmallButton: defaultTableSmallButton;
darkGiftCodeBox: Box(giveawayGiftCodeBox) {
bg: groupCallMembersBg;
title: FlatLabel(boxTitle) {
textFg: groupCallMembersFg;
}
titleAdditionalFg: groupCallMemberNotJoinedStatus;
}
darkGiftLink: icon {{ "menu/copy", groupCallMembersFg }};
darkGiftShare: icon {{ "menu/share", groupCallMembersFg }};
darkGiftTheme: icon {{ "menu/colors", groupCallMembersFg }};
darkGiftTransfer: icon {{ "chat/input_replace", groupCallMembersFg }};
darkGiftNftWear: icon {{ "menu/nft_wear", groupCallMembersFg }};
darkGiftNftTakeOff: icon {{ "menu/nft_takeoff", groupCallMembersFg }};
darkGiftNftResell: icon {{ "menu/tag_sell", groupCallMembersFg }};
darkGiftNftUnlist: icon {{ "menu/tag_remove", groupCallMembersFg }};
darkGiftHide: icon {{ "menu/stealth", groupCallMembersFg }};
darkGiftShow: icon {{ "menu/show_in_chat", groupCallMembersFg }};
darkGiftPin: icon {{ "menu/pin", groupCallMembersFg }};
darkGiftUnpin: icon {{ "menu/unpin", groupCallMembersFg }};
darkGiftOffer: icon {{ "menu/earn", groupCallMembersFg }};
darkGiftPalette: TextPalette(defaultTextPalette) {
linkFg: mediaviewTextLinkFg;
monoFg: groupCallMembersFg;
spoilerFg: groupCallMembersFg;
}
darkGiftTable: Table(giveawayGiftCodeTable) {
headerBg: groupCallMembersBgOver;
borderFg: mediaviewMenuBgOver;
smallButton: RoundButton(defaultTableSmallButton) {
textFg: groupCallMembersFg;
textFgOver: groupCallMembersFg;
textBg: groupCallMenuBgRipple;
textBgOver: groupCallMenuBgRipple;
ripple: RippleAnimation(defaultRippleAnimation) {
color: mediaviewMenuBgOver;
}
}
defaultLabel: FlatLabel(defaultTableLabel) {
textFg: groupCallMembersFg;
palette: darkGiftPalette;
}
defaultValue: FlatLabel(defaultTableValue) {
textFg: groupCallMembersFg;
palette: darkGiftPalette;
}
}
darkGiftTableValueMultiline: FlatLabel(giveawayGiftCodeValueMultiline) {
textFg: groupCallMembersFg;
palette: darkGiftPalette;
}
darkGiftTableMessage: FlatLabel(giveawayGiftMessage) {
textFg: groupCallMembersFg;
palette: darkGiftPalette;
}
darkGiftCodeLink: FlatLabel(giveawayGiftCodeLink) {
textFg: mediaviewMenuFg;
}
darkGiftBoxClose: IconButton(boxTitleClose) {
icon: icon {{ "box_button_close", groupCallMemberInactiveIcon }};
iconOver: icon {{ "box_button_close", groupCallMemberInactiveIcon }};
ripple: RippleAnimation(defaultRippleAnimation) {
color: groupCallMembersBgOver;
}
}

View File

@@ -0,0 +1,375 @@
/*
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 "info/channel_statistics/boosts/giveaway/giveaway_list_controllers.h"
#include "apiwrap.h"
#include "data/data_channel.h"
#include "data/data_folder.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "dialogs/dialogs_indexed_list.h"
#include "history/history.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/boxes/confirm_box.h"
#include "ui/effects/ripple_animation.h"
#include "ui/painter.h"
#include "styles/style_giveaway.h"
namespace Giveaway {
namespace {
class ChannelRow final : public PeerListRow {
public:
using PeerListRow::PeerListRow;
QSize rightActionSize() const override;
QMargins rightActionMargins() const override;
void rightActionPaint(
Painter &p,
int x,
int y,
int outerWidth,
bool selected,
bool actionSelected) override;
void rightActionAddRipple(
QPoint point,
Fn<void()> updateCallback) override;
void rightActionStopLastRipple() override;
private:
std::unique_ptr<Ui::RippleAnimation> _actionRipple;
};
QSize ChannelRow::rightActionSize() const {
return QSize(
st::giveawayGiftCodeChannelDeleteIcon.width(),
st::giveawayGiftCodeChannelDeleteIcon.height()) * 2;
}
QMargins ChannelRow::rightActionMargins() const {
const auto itemHeight = st::giveawayGiftCodeChannelsPeerList.item.height;
return QMargins(
0,
(itemHeight - rightActionSize().height()) / 2,
st::giveawayRadioPosition.x() / 2,
0);
}
void ChannelRow::rightActionPaint(
Painter &p,
int x,
int y,
int outerWidth,
bool selected,
bool actionSelected) {
if (_actionRipple) {
_actionRipple->paint(
p,
x,
y,
outerWidth);
if (_actionRipple->empty()) {
_actionRipple.reset();
}
}
const auto rect = QRect(QPoint(x, y), ChannelRow::rightActionSize());
(actionSelected
? st::giveawayGiftCodeChannelDeleteIconOver
: st::giveawayGiftCodeChannelDeleteIcon).paintInCenter(p, rect);
}
void ChannelRow::rightActionAddRipple(
QPoint point,
Fn<void()> updateCallback) {
if (!_actionRipple) {
auto mask = Ui::RippleAnimation::EllipseMask(rightActionSize());
_actionRipple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
std::move(mask),
std::move(updateCallback));
}
_actionRipple->add(point);
}
void ChannelRow::rightActionStopLastRipple() {
if (_actionRipple) {
_actionRipple->lastStop();
}
}
} // namespace
AwardMembersListController::AwardMembersListController(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
std::vector<not_null<PeerData*>> selected)
: ParticipantsBoxController(navigation, peer, ParticipantsRole::Members)
, _selected(std::move(selected)) {
}
void AwardMembersListController::prepare() {
ParticipantsBoxController::prepare();
delegate()->peerListAddSelectedPeers(base::take(_selected));
delegate()->peerListRefreshRows();
}
void AwardMembersListController::rowClicked(not_null<PeerListRow*> row) {
const auto checked = !row->checked();
if (checked
&& _checkErrorCallback
&& _checkErrorCallback(delegate()->peerListSelectedRowsCount())) {
return;
}
delegate()->peerListSetRowChecked(row, checked);
}
std::unique_ptr<PeerListRow> AwardMembersListController::createRow(
not_null<PeerData*> participant) const {
const auto user = participant->asUser();
if (!user || user->isInaccessible() || user->isBot() || user->isSelf()) {
return nullptr;
}
return std::make_unique<PeerListRow>(participant);
}
base::unique_qptr<Ui::PopupMenu> AwardMembersListController::rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) {
return nullptr;
}
void AwardMembersListController::setCheckError(Fn<bool(int)> callback) {
_checkErrorCallback = std::move(callback);
}
MyChannelsListController::MyChannelsListController(
not_null<PeerData*> peer,
std::shared_ptr<Ui::Show> show,
std::vector<not_null<PeerData*>> selected)
: PeerListController(
std::make_unique<PeerListGlobalSearchController>(&peer->session()))
, _peer(peer)
, _show(show)
, _selected(std::move(selected))
, _otherChannels(std::make_unique<std::vector<not_null<ChannelData*>>>()) {
{
const auto addList = [&](not_null<Dialogs::IndexedList*> list) {
for (const auto &row : list->all()) {
if (const auto history = row->history()) {
const auto channel = history->peer->asChannel();
if (channel && !channel->isMegagroup()) {
_otherChannels->push_back(channel);
}
}
}
};
auto &data = _peer->owner();
addList(data.chatsList()->indexed());
if (const auto folder = data.folderLoaded(Data::Folder::kId)) {
addList(folder->chatsList()->indexed());
}
addList(data.contactsNoChatsList());
}
}
std::unique_ptr<PeerListRow> MyChannelsListController::createSearchRow(
not_null<PeerData*> peer) {
if (const auto channel = peer->asChannel()) {
return createRow(channel);
}
return nullptr;
}
std::unique_ptr<PeerListRow> MyChannelsListController::createRestoredRow(
not_null<PeerData*> peer) {
if (const auto channel = peer->asChannel()) {
return createRow(channel);
}
return nullptr;
}
void MyChannelsListController::loadMoreRows() {
if (_apiLifetime || !_otherChannels) {
return;
} else if (_lastAddedIndex >= _otherChannels->size()) {
_otherChannels.release();
return;
}
constexpr auto kPerPage = int(40);
const auto till = std::min(
int(_otherChannels->size()),
_lastAddedIndex + kPerPage);
while (_lastAddedIndex < till) {
delegate()->peerListAppendRow(
createRow(_otherChannels->at(_lastAddedIndex++)));
}
delegate()->peerListRefreshRows();
}
void MyChannelsListController::rowClicked(not_null<PeerListRow*> row) {
const auto channel = row->peer()->asChannel();
const auto checked = !row->checked();
if (checked
&& _checkErrorCallback
&& _checkErrorCallback(delegate()->peerListSelectedRowsCount())) {
return;
}
if (checked && channel && channel->username().isEmpty()) {
_show->showBox(Box(Ui::ConfirmBox, Ui::ConfirmBoxArgs{
.text = tr::lng_giveaway_channels_confirm_about(),
.confirmed = [=](Fn<void()> close) {
delegate()->peerListSetRowChecked(row, checked);
close();
},
.confirmText = tr::lng_filters_recommended_add(),
.title = tr::lng_giveaway_channels_confirm_title(),
}));
} else {
delegate()->peerListSetRowChecked(row, checked);
}
}
Main::Session &MyChannelsListController::session() const {
return _peer->session();
}
void MyChannelsListController::prepare() {
delegate()->peerListSetSearchMode(PeerListSearchMode::Enabled);
const auto api = _apiLifetime.make_state<MTP::Sender>(
&session().api().instance());
api->request(
MTPstories_GetChatsToSend()
).done([=](const MTPmessages_Chats &result) {
_apiLifetime.destroy();
const auto &chats = result.match([](const auto &data) {
return data.vchats().v;
});
auto &owner = session().data();
for (const auto &chat : chats) {
if (const auto peer = owner.processChat(chat)) {
if (!peer->isChannel() || (peer == _peer)) {
continue;
}
if (!delegate()->peerListFindRow(peer->id.value)) {
if (const auto channel = peer->asChannel()) {
auto row = createRow(channel);
const auto raw = row.get();
delegate()->peerListAppendRow(std::move(row));
if (ranges::contains(_selected, peer)) {
delegate()->peerListSetRowChecked(raw, true);
_selected.erase(
ranges::remove(_selected, peer),
end(_selected));
}
}
}
}
}
for (const auto &selected : _selected) {
if (const auto channel = selected->asChannel()) {
auto row = createRow(channel);
const auto raw = row.get();
delegate()->peerListAppendRow(std::move(row));
delegate()->peerListSetRowChecked(raw, true);
}
}
delegate()->peerListRefreshRows();
_selected.clear();
}).send();
}
void MyChannelsListController::setCheckError(Fn<bool(int)> callback) {
_checkErrorCallback = std::move(callback);
}
std::unique_ptr<PeerListRow> MyChannelsListController::createRow(
not_null<ChannelData*> channel) const {
auto row = std::make_unique<PeerListRow>(channel);
row->setCustomStatus((channel->isBroadcast()
? tr::lng_chat_status_subscribers
: tr::lng_chat_status_members)(
tr::now,
lt_count,
channel->membersCount()));
return row;
}
SelectedChannelsListController::SelectedChannelsListController(
not_null<PeerData*> peer)
: _peer(peer) {
PeerListController::setStyleOverrides(
&st::giveawayGiftCodeChannelsPeerList);
}
void SelectedChannelsListController::setTopStatus(rpl::producer<QString> s) {
_statusLifetime = std::move(
s
) | rpl::on_next([=](const QString &t) {
if (delegate()->peerListFullRowsCount() > 0) {
delegate()->peerListRowAt(0)->setCustomStatus(t);
}
});
}
void SelectedChannelsListController::rebuild(
std::vector<not_null<PeerData*>> selected) {
while (delegate()->peerListFullRowsCount() > 1) {
delegate()->peerListRemoveRow(delegate()->peerListRowAt(1));
}
for (const auto &peer : selected) {
delegate()->peerListAppendRow(createRow(peer->asChannel()));
}
delegate()->peerListRefreshRows();
}
auto SelectedChannelsListController::channelRemoved() const
-> rpl::producer<not_null<PeerData*>> {
return _channelRemoved.events();
}
void SelectedChannelsListController::rowClicked(not_null<PeerListRow*> row) {
}
void SelectedChannelsListController::rowRightActionClicked(
not_null<PeerListRow*> row) {
const auto peer = row->peer();
delegate()->peerListRemoveRow(row);
delegate()->peerListRefreshRows();
_channelRemoved.fire_copy(peer);
}
Main::Session &SelectedChannelsListController::session() const {
return _peer->session();
}
void SelectedChannelsListController::prepare() {
delegate()->peerListAppendRow(createRow(_peer->asChannel()));
}
std::unique_ptr<PeerListRow> SelectedChannelsListController::createRow(
not_null<ChannelData*> channel) const {
const auto isYourChannel = (_peer->asChannel() == channel);
auto row = isYourChannel
? std::make_unique<PeerListRow>(channel)
: std::make_unique<ChannelRow>(channel);
row->setCustomStatus(isYourChannel
? QString()
: (channel->isMegagroup()
? tr::lng_chat_status_members
: tr::lng_chat_status_subscribers)(
tr::now,
lt_count,
channel->membersCount()));
return row;
}
} // namespace Giveaway

View File

@@ -0,0 +1,113 @@
/*
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 "boxes/peers/edit_participants_box.h"
class ChannelData;
class PeerData;
class PeerListRow;
namespace Ui {
class PopupMenu;
class Show;
} // namespace Ui
namespace Window {
class SessionNavigation;
} // namespace Window
namespace Giveaway {
class AwardMembersListController : public ParticipantsBoxController {
public:
AwardMembersListController(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
std::vector<not_null<PeerData*>> selected);
void prepare() override;
void setCheckError(Fn<bool(int)> callback);
void rowClicked(not_null<PeerListRow*> row) override;
std::unique_ptr<PeerListRow> createRow(
not_null<PeerData*> participant) const override;
base::unique_qptr<Ui::PopupMenu> rowContextMenu(
QWidget *parent,
not_null<PeerListRow*> row) override;
private:
Fn<bool(int)> _checkErrorCallback;
std::vector<not_null<PeerData*>> _selected;
};
class MyChannelsListController : public PeerListController {
public:
MyChannelsListController(
not_null<PeerData*> peer,
std::shared_ptr<Ui::Show> show,
std::vector<not_null<PeerData*>> selected);
void setCheckError(Fn<bool(int)> callback);
Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
void loadMoreRows() override;
std::unique_ptr<PeerListRow> createSearchRow(
not_null<PeerData*> peer) override;
std::unique_ptr<PeerListRow> createRestoredRow(
not_null<PeerData*> peer) override;
private:
std::unique_ptr<PeerListRow> createRow(
not_null<ChannelData*> channel) const;
const not_null<PeerData*> _peer;
const std::shared_ptr<Ui::Show> _show;
Fn<bool(int)> _checkErrorCallback;
std::vector<not_null<PeerData*>> _selected;
std::unique_ptr<std::vector<not_null<ChannelData*>>> _otherChannels;
int _lastAddedIndex = 0;
rpl::lifetime _apiLifetime;
};
class SelectedChannelsListController : public PeerListController {
public:
SelectedChannelsListController(not_null<PeerData*> peer);
void setTopStatus(rpl::producer<QString> status);
void rebuild(std::vector<not_null<PeerData*>> selected);
[[nodiscard]] rpl::producer<not_null<PeerData*>> channelRemoved() const;
Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
void rowRightActionClicked(not_null<PeerListRow*> row) override;
private:
std::unique_ptr<PeerListRow> createRow(
not_null<ChannelData*> channel) const;
const not_null<PeerData*> _peer;
rpl::event_stream<not_null<PeerData*>> _channelRemoved;
rpl::lifetime _statusLifetime;
};
} // namespace Giveaway

View File

@@ -0,0 +1,194 @@
/*
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 "info/channel_statistics/boosts/giveaway/giveaway_type_row.h"
#include "lang/lang_keys.h"
#include "ui/effects/credits_graphics.h"
#include "ui/effects/premium_graphics.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/text/text_options.h"
#include "ui/widgets/checkbox.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "styles/style_color_indices.h"
#include "styles/style_giveaway.h"
#include "styles/style_statistics.h"
namespace Giveaway {
GiveawayTypeRow::GiveawayTypeRow(
not_null<Ui::RpWidget*> parent,
Type type,
rpl::producer<QString> subtitle,
bool group)
: GiveawayTypeRow(
parent,
type,
(type == Type::SpecificUsers) ? st::colorIndexBlue : st::colorIndexGreen,
(type == Type::SpecificUsers)
? tr::lng_giveaway_award_option()
: (type == Type::Random)
? tr::lng_premium_summary_title()
// ? tr::lng_giveaway_create_option()
: (type == Type::AllMembers)
? (group
? tr::lng_giveaway_users_all_group()
: tr::lng_giveaway_users_all())
: (group
? tr::lng_giveaway_users_new_group()
: tr::lng_giveaway_users_new()),
std::move(subtitle),
QImage()) {
}
GiveawayTypeRow::GiveawayTypeRow(
not_null<Ui::RpWidget*> parent,
Type type,
int colorIndex,
rpl::producer<QString> title,
rpl::producer<QString> subtitle,
QImage badge)
: RippleButton(parent, st::defaultRippleAnimation)
, _type(type)
, _st((_type == Type::SpecificUsers
|| _type == Type::Random
|| _type == Type::Credits)
? st::giveawayTypeListItem
: ((_type == Type::Prepaid) || (_type == Type::PrepaidCredits))
? st::boostsListBox.item
: st::giveawayGiftCodeMembersPeerList.item)
, _userpic(
Ui::EmptyUserpic::UserpicColor(Ui::EmptyUserpic::ColorIndex(colorIndex)),
QString())
, _badge(std::move(badge)) {
if (_type == Type::Credits || _type == Type::PrepaidCredits) {
_customUserpic = Ui::CreditsWhiteDoubledIcon(_st.photoSize, 1.);
}
std::move(
subtitle
) | rpl::on_next([=] (QString s) {
_status.setText(
st::defaultTextStyle,
s.replace(QChar('>'), QString()),
Ui::NameTextOptions());
}, lifetime());
std::move(
title
) | rpl::on_next([=] (const QString &s) {
_name.setText(_st.nameStyle, s, Ui::NameTextOptions());
}, lifetime());
}
int GiveawayTypeRow::resizeGetHeight(int) {
return _st.height;
}
void GiveawayTypeRow::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
const auto paintOver = (isOver() || isDown()) && !isDisabled();
const auto skipRight = _st.photoPosition.x();
const auto outerWidth = width();
const auto isRandom = (_type == Type::Random);
const auto isSpecific = (_type == Type::SpecificUsers);
const auto isPrepaid = (_type == Type::Prepaid);
const auto hasUserpic = isRandom
|| isSpecific
|| isPrepaid
|| (!_customUserpic.isNull());
if (paintOver) {
p.fillRect(e->rect(), _st.button.textBgOver);
}
Ui::RippleButton::paintRipple(p, 0, 0);
if (hasUserpic) {
_userpic.paintCircle(
p,
_st.photoPosition.x(),
_st.photoPosition.y(),
outerWidth,
_st.photoSize);
const auto userpicRect = QRect(
_st.photoPosition
- QPoint(
isSpecific ? -st::giveawayUserpicSkip : 0,
isSpecific ? 0 : st::giveawayUserpicSkip),
Size(_st.photoSize));
if (!_customUserpic.isNull()) {
p.drawImage(_st.photoPosition, _customUserpic);
} else {
const auto &userpic = isSpecific
? st::giveawayUserpicGroup
: st::giveawayUserpic;
userpic.paintInCenter(p, userpicRect);
}
}
const auto namex = _st.namePosition.x();
const auto namey = _st.namePosition.y();
const auto namew = outerWidth - namex - skipRight;
const auto badgew = _badge.width() / style::DevicePixelRatio();
p.setPen(_st.nameFg);
_name.drawLeftElided(p, namex, namey, namew - badgew, width());
if (!_badge.isNull()) {
p.drawImage(
std::min(
namex + _name.maxWidth() + st::boostsListBadgePadding.left(),
outerWidth - badgew - skipRight),
namey + st::boostsListMiniIconSkip,
_badge);
}
const auto statusIcon = isRandom ? &st::topicButtonArrow : nullptr;
const auto statusx = _st.statusPosition.x();
const auto statusy = _st.statusPosition.y();
const auto statusw = outerWidth
- statusx
- skipRight
- (statusIcon
? (statusIcon->width() + st::boostsListMiniIconSkip)
: 0);
p.setFont(st::contactsStatusFont);
p.setPen((isRandom || !hasUserpic) ? st::lightButtonFg : _st.statusFg);
_status.drawLeftElided(p, statusx, statusy, statusw, outerWidth);
if (statusIcon) {
statusIcon->paint(
p,
QPoint(
statusx
+ std::min(_status.maxWidth(), statusw)
+ st::boostsListMiniIconSkip,
statusy + st::contactsStatusFont->descent),
outerWidth,
st::lightButtonFg->c);
}
}
void GiveawayTypeRow::addRadio(
std::shared_ptr<Ui::RadioenumGroup<Type>> typeGroup) {
const auto &st = st::defaultCheckbox;
const auto radio = Ui::CreateChild<Ui::Radioenum<Type>>(
this,
std::move(typeGroup),
_type,
QString(),
st);
const auto pos = (_type == Type::SpecificUsers || _type == Type::Random)
? st::giveawayRadioPosition
: st::giveawayRadioMembersPosition;
radio->moveToLeft(pos.x(), pos.y());
radio->setAttribute(Qt::WA_TransparentForMouseEvents);
radio->show();
}
} // namespace Giveaway

View File

@@ -0,0 +1,70 @@
/*
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/empty_userpic.h"
#include "ui/widgets/buttons.h"
namespace Ui {
template <typename Enum>
class RadioenumGroup;
} // namespace Ui
namespace Giveaway {
class GiveawayTypeRow final : public Ui::RippleButton {
public:
enum class Type {
Random,
SpecificUsers,
AllMembers,
OnlyNewMembers,
Prepaid,
PrepaidCredits,
Credits,
};
GiveawayTypeRow(
not_null<Ui::RpWidget*> parent,
Type type,
rpl::producer<QString> subtitle,
bool group);
GiveawayTypeRow(
not_null<Ui::RpWidget*> parent,
Type type,
int colorIndex,
rpl::producer<QString> title,
rpl::producer<QString> subtitle,
QImage badge);
void addRadio(std::shared_ptr<Ui::RadioenumGroup<Type>> typeGroup);
protected:
void paintEvent(QPaintEvent *e) override;
int resizeGetHeight(int) override;
private:
const Type _type;
const style::PeerListItem _st;
Ui::EmptyUserpic _userpic;
Ui::Text::String _status;
Ui::Text::String _name;
QImage _customUserpic;
QImage _badge;
};
} // namespace Giveaway

View File

@@ -0,0 +1,220 @@
/*
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 "info/channel_statistics/boosts/giveaway/select_countries_box.h"
#include "countries/countries_instance.h"
#include "lang/lang_keys.h"
#include "ui/emoji_config.h"
#include "ui/layers/generic_box.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/vertical_list.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/multi_select.h"
#include "ui/wrap/slide_wrap.h"
#include "styles/style_boxes.h"
#include "styles/style_giveaway.h"
#include "styles/style_settings.h"
namespace Ui {
namespace {
[[nodiscard]] QImage CacheFlagEmoji(const QString &flag) {
const auto &st = st::giveawayGiftCodeCountrySelect.item;
auto roundPaintCache = QImage(
Size(st.height) * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
roundPaintCache.setDevicePixelRatio(style::DevicePixelRatio());
roundPaintCache.fill(Qt::transparent);
{
const auto size = st.height;
auto p = Painter(&roundPaintCache);
auto hq = PainterHighQualityEnabler(p);
const auto flagText = Ui::Text::String(st::defaultTextStyle, flag);
p.setPen(st.textBg);
p.setBrush(st.textBg);
p.drawEllipse(0, 0, size, size);
flagText.draw(p, {
.position = QPoint(
0 + (size - flagText.maxWidth()) / 2,
0 + (size - flagText.minHeight()) / 2),
.outerWidth = size,
.availableWidth = size,
});
}
return roundPaintCache;
}
} // namespace
void SelectCountriesBox(
not_null<Ui::GenericBox*> box,
const std::vector<QString> &selected,
Fn<void(std::vector<QString>)> doneCallback,
Fn<bool(int)> checkErrorCallback) {
struct State final {
std::vector<QString> resultList;
};
const auto state = box->lifetime().make_state<State>();
const auto multiSelect = box->setPinnedToTopContent(
object_ptr<Ui::MultiSelect>(
box,
st::giveawayGiftCodeCountrySelect,
tr::lng_participant_filter()));
Ui::AddSkip(box->verticalLayout());
const auto &buttonSt = st::giveawayGiftCodeCountryButton;
struct Entry final {
Ui::SlideWrap<Ui::SettingsButton> *wrap = nullptr;
QStringList list;
QString iso2;
};
auto countries = Countries::Instance().list();
ranges::sort(countries, [](
const Countries::Info &a,
const Countries::Info &b) {
return (a.name.compare(b.name, Qt::CaseInsensitive) < 0);
});
auto buttons = std::vector<Entry>();
buttons.reserve(countries.size());
for (const auto &country : countries) {
const auto flag = Countries::Instance().flagEmojiByISO2(country.iso2);
if (!Ui::Emoji::Find(flag)) {
continue;
}
const auto itemId = buttons.size();
auto button = object_ptr<SettingsButton>(
box->verticalLayout(),
rpl::single(flag + ' ' + country.name),
buttonSt);
const auto radio = Ui::CreateChild<Ui::RpWidget>(button.data());
const auto radioView = std::make_shared<Ui::RadioView>(
st::defaultRadio,
false,
[=] { radio->update(); });
{
const auto radioSize = radioView->getSize();
radio->resize(radioSize);
radio->paintRequest(
) | rpl::on_next([=](const QRect &r) {
auto p = QPainter(radio);
radioView->paint(p, 0, 0, radioSize.width());
}, radio->lifetime());
const auto buttonHeight = buttonSt.height
+ rect::m::sum::v(buttonSt.padding);
radio->moveToLeft(
st::giveawayRadioPosition.x(),
(buttonHeight - radioSize.height()) / 2);
}
const auto roundPaintCache = CacheFlagEmoji(flag);
const auto paintCallback = [=](Painter &p, int x, int y, int, int) {
p.drawImage(x, y, roundPaintCache);
};
const auto choose = [=](bool clicked) {
const auto value = !radioView->checked();
if (value && checkErrorCallback(state->resultList.size())) {
return;
}
radioView->setChecked(value, anim::type::normal);
if (value) {
state->resultList.push_back(country.iso2);
multiSelect->addItem(
itemId,
country.name,
st::activeButtonBg,
paintCallback,
clicked
? Ui::MultiSelect::AddItemWay::Default
: Ui::MultiSelect::AddItemWay::SkipAnimation);
} else {
auto &list = state->resultList;
list.erase(ranges::remove(list, country.iso2), end(list));
multiSelect->removeItem(itemId);
}
};
button->setClickedCallback([=] {
choose(true);
});
if (ranges::contains(selected, country.iso2)) {
choose(false);
}
const auto wrap = box->verticalLayout()->add(
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
box,
std::move(button)));
wrap->toggle(true, anim::type::instant);
{
auto list = QStringList{
flag,
country.name,
country.alternativeName,
};
buttons.push_back({ wrap, std::move(list), country.iso2 });
}
}
const auto noResults = box->addRow(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
box,
object_ptr<Ui::VerticalLayout>(box)));
{
noResults->toggle(false, anim::type::instant);
const auto container = noResults->entity();
Ui::AddSkip(container);
Ui::AddSkip(container);
container->add(
object_ptr<Ui::FlatLabel>(
container,
tr::lng_search_messages_none(),
st::membersAbout),
style::al_top);
Ui::AddSkip(container);
Ui::AddSkip(container);
}
multiSelect->setQueryChangedCallback([=](const QString &query) {
auto wasAnyFound = false;
for (const auto &entry : buttons) {
const auto found = ranges::any_of(entry.list, [&](
const QString &s) {
return s.startsWith(query, Qt::CaseInsensitive);
});
entry.wrap->toggle(found, anim::type::instant);
wasAnyFound |= found;
}
noResults->toggle(!wasAnyFound, anim::type::instant);
});
multiSelect->setItemRemovedCallback([=](uint64 itemId) {
auto &list = state->resultList;
auto &button = buttons[itemId];
const auto it = ranges::find(list, button.iso2);
if (it != end(list)) {
list.erase(it);
button.wrap->entity()->clicked({}, Qt::LeftButton);
}
});
box->addButton(tr::lng_settings_save(), [=] {
doneCallback(state->resultList);
box->closeBox();
});
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
}
} // namespace Ui

View File

@@ -0,0 +1,20 @@
/*
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
namespace Ui {
class GenericBox;
void SelectCountriesBox(
not_null<Ui::GenericBox*> box,
const std::vector<QString> &selected,
Fn<void(std::vector<QString>)> doneCallback,
Fn<bool(int)> checkErrorCallback);
} // namespace Ui

View File

@@ -0,0 +1,571 @@
/*
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 "info/channel_statistics/boosts/info_boosts_inner_widget.h"
#include "api/api_premium.h"
#include "api/api_statistics.h"
#include "boxes/gift_premium_box.h"
#include "boxes/peers/edit_peer_invite_link.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "info/channel_statistics/boosts/create_giveaway_box.h"
#include "info/channel_statistics/boosts/giveaway/boost_badge.h"
#include "info/channel_statistics/boosts/giveaway/giveaway_type_row.h"
#include "info/channel_statistics/boosts/info_boosts_widget.h"
#include "info/info_controller.h"
#include "info/profile/info_profile_icon.h"
#include "info/statistics/info_statistics_inner_widget.h" // FillLoading.
#include "info/statistics/info_statistics_list_controllers.h"
#include "lang/lang_keys.h"
#include "settings/settings_credits_graphics.h"
#include "statistics/widgets/chart_header_widget.h"
#include "ui/boxes/boost_box.h"
#include "ui/controls/invite_link_label.h"
#include "ui/effects/ripple_animation.h"
#include "ui/empty_userpic.h"
#include "ui/layers/generic_box.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/vertical_list.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/shadow.h"
#include "ui/widgets/slider_natural_width.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/ui_utility.h"
#include "styles/style_color_indices.h"
#include "styles/style_dialogs.h" // dialogsSearchTabs
#include "styles/style_giveaway.h"
#include "styles/style_info.h"
#include "styles/style_premium.h"
#include "styles/style_statistics.h"
#include <QtGui/QGuiApplication>
namespace Info::Boosts {
namespace {
void AddHeader(
not_null<Ui::VerticalLayout*> content,
tr::phrase<> text) {
const auto header = content->add(
object_ptr<Statistic::Header>(content),
st::statisticsLayerMargins + st::boostsChartHeaderPadding);
header->resizeToWidth(header->width());
header->setTitle(text(tr::now));
header->setSubTitle({});
}
void FillOverview(
not_null<Ui::VerticalLayout*> content,
const Data::BoostStatus &status) {
const auto &stats = status.overview;
Ui::AddSkip(content, st::boostsLayerOverviewMargins.top());
AddHeader(content, tr::lng_stats_overview_title);
Ui::AddSkip(content);
const auto diffBetweenHeaders = 0
+ st::statisticsOverviewValue.style.font->height
- st::statisticsHeaderTitleTextStyle.font->height;
const auto container = content->add(
object_ptr<Ui::RpWidget>(content),
st::statisticsLayerMargins);
const auto addPrimary = [&](float64 v, bool approximately = false) {
return Ui::CreateChild<Ui::FlatLabel>(
container,
(v >= 0)
? (approximately && v ? QChar(0x2248) : QChar())
+ Lang::FormatCountToShort(v).string
: QString(),
st::statisticsOverviewValue);
};
const auto addSub = [&](
not_null<Ui::RpWidget*> primary,
float64 percentage,
tr::phrase<> text) {
const auto second = Ui::CreateChild<Ui::FlatLabel>(
container,
percentage
? u"%1%"_q.arg(std::abs(std::round(percentage * 10.) / 10.))
: QString(),
st::statisticsOverviewSecondValue);
second->setTextColorOverride(st::windowSubTextFg->c);
const auto sub = Ui::CreateChild<Ui::FlatLabel>(
container,
text(),
st::statisticsOverviewSubtext);
sub->setTextColorOverride(st::windowSubTextFg->c);
primary->geometryValue(
) | rpl::on_next([=](const QRect &g) {
const auto &padding = st::statisticsOverviewSecondValuePadding;
second->moveToLeft(
rect::right(g) + padding.left(),
g.y() + padding.top());
sub->moveToLeft(
g.x(),
st::statisticsChartHeaderHeight
- st::statisticsOverviewSubtext.style.font->height
+ g.y()
+ diffBetweenHeaders);
}, primary->lifetime());
};
const auto topLeftLabel = addPrimary(stats.level);
const auto topRightLabel = addPrimary(stats.premiumMemberCount, true);
const auto bottomLeftLabel = addPrimary(stats.boostCount);
const auto bottomRightLabel = addPrimary(std::max(
stats.nextLevelBoostCount - stats.boostCount,
0));
addSub(
topLeftLabel,
0,
tr::lng_boosts_level);
addSub(
topRightLabel,
stats.premiumMemberPercentage,
(stats.group
? tr::lng_boosts_premium_members
: tr::lng_boosts_premium_audience));
addSub(
bottomLeftLabel,
0,
tr::lng_boosts_existing);
addSub(
bottomRightLabel,
0,
tr::lng_boosts_next_level);
container->showChildren();
container->resize(container->width(), topLeftLabel->height() * 5);
container->sizeValue(
) | rpl::on_next([=](const QSize &s) {
const auto halfWidth = s.width() / 2;
{
const auto &p = st::boostsOverviewValuePadding;
topLeftLabel->moveToLeft(p.left(), p.top());
}
topRightLabel->moveToLeft(
topLeftLabel->x() + halfWidth + st::statisticsOverviewRightSkip,
topLeftLabel->y());
bottomLeftLabel->moveToLeft(
topLeftLabel->x(),
topLeftLabel->y() + st::statisticsOverviewMidSkip);
bottomRightLabel->moveToLeft(
topRightLabel->x(),
bottomLeftLabel->y());
}, container->lifetime());
Ui::AddSkip(content, st::boostsLayerOverviewMargins.bottom());
}
void FillShareLink(
not_null<Ui::VerticalLayout*> content,
std::shared_ptr<Ui::Show> show,
const QString &link,
not_null<PeerData*> peer) {
const auto weak = base::make_weak(content);
const auto copyLink = crl::guard(weak, [=] {
QGuiApplication::clipboard()->setText(link);
show->showToast(tr::lng_channel_public_link_copied(tr::now));
});
const auto shareLink = crl::guard(weak, [=] {
show->showBox(ShareInviteLinkBox(peer, link));
});
const auto label = content->lifetime().make_state<Ui::InviteLinkLabel>(
content,
rpl::single(base::duplicate(link).replace(u"https://"_q, QString())),
nullptr);
content->add(
label->take(),
st::boostsLinkFieldPadding);
label->clicks(
) | rpl::on_next(copyLink, label->lifetime());
{
const auto wrap = content->add(
object_ptr<Ui::FixedHeightWidget>(
content,
st::inviteLinkButton.height),
st::inviteLinkButtonsPadding);
const auto copy = CreateChild<Ui::RoundButton>(
wrap,
tr::lng_group_invite_context_copy(),
st::inviteLinkCopy);
copy->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
copy->setClickedCallback(copyLink);
const auto share = CreateChild<Ui::RoundButton>(
wrap,
tr::lng_group_invite_context_share(),
st::inviteLinkShare);
share->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
share->setClickedCallback(shareLink);
wrap->widthValue(
) | rpl::on_next([=](int width) {
const auto buttonWidth = (width - st::inviteLinkButtonsSkip) / 2;
copy->setFullWidth(buttonWidth);
share->setFullWidth(buttonWidth);
copy->moveToLeft(0, 0, width);
share->moveToRight(0, 0, width);
}, wrap->lifetime());
wrap->showChildren();
}
Ui::AddSkip(content, st::boostsLinkFieldPadding.bottom());
}
void FillGetBoostsButton(
not_null<Ui::VerticalLayout*> content,
not_null<Controller*> controller,
std::shared_ptr<Ui::Show> show,
not_null<PeerData*> peer,
Fn<void()> reloadOnDone) {
if (!Api::PremiumGiftCodeOptions(peer).giveawayGiftsPurchaseAvailable()) {
return;
}
Ui::AddSkip(content);
const auto &st = st::getBoostsButton;
const auto &icon = st::getBoostsButtonIcon;
const auto button = content->add(object_ptr<Ui::SettingsButton>(
content.get(),
tr::lng_boosts_get_boosts(),
st));
button->setClickedCallback([=] {
show->showBox(Box(
CreateGiveawayBox,
controller,
peer,
reloadOnDone,
std::nullopt));
});
Ui::CreateChild<Info::Profile::FloatingIcon>(
button,
icon,
QPoint{
st::infoSharedMediaButtonIconPosition.x(),
(st.height + rect::m::sum::v(st.padding) - icon.height()) / 2,
})->show();
Ui::AddSkip(content);
Ui::AddDividerText(
content,
peer->isMegagroup()
? tr::lng_boosts_get_boosts_subtext_group()
: tr::lng_boosts_get_boosts_subtext());
}
} // namespace
InnerWidget::InnerWidget(
QWidget *parent,
not_null<Controller*> controller,
not_null<PeerData*> peer)
: VerticalLayout(parent)
, _controller(controller)
, _peer(peer)
, _show(controller->uiShow()) {
}
void InnerWidget::load() {
const auto api = lifetime().make_state<Api::Boosts>(_peer);
Info::Statistics::FillLoading(
this,
Info::Statistics::LoadingType::Boosts,
_loaded.events_starting_with(false) | rpl::map(!rpl::mappers::_1),
_showFinished.events());
_showFinished.events(
) | rpl::take(1) | rpl::on_next([=] {
api->request(
) | rpl::on_error_done([](const QString &error) {
}, [=] {
_state = api->boostStatus();
_loaded.fire(true);
fill();
}, lifetime());
}, lifetime());
}
void InnerWidget::fill() {
const auto fakeShowed = lifetime().make_state<rpl::event_stream<>>();
const auto &status = _state;
const auto inner = this;
const auto reloadOnDone = crl::guard(this, [=] {
while (Ui::VerticalLayout::count()) {
delete Ui::VerticalLayout::widgetAt(0);
}
load();
_showFinished.fire({});
});
{
auto dividerContent = object_ptr<Ui::VerticalLayout>(inner);
dividerContent->add(object_ptr<Ui::FixedHeightWidget>(
dividerContent,
st::boostSkipTop));
Ui::FillBoostLimit(
fakeShowed->events(),
dividerContent.data(),
rpl::single(Ui::BoostCounters{
.level = status.overview.level,
.boosts = status.overview.boostCount,
.thisLevelBoosts
= status.overview.currentLevelBoostCount,
.nextLevelBoosts
= status.overview.nextLevelBoostCount,
.mine = status.overview.mine,
}),
st::statisticsLimitsLinePadding);
inner->add(object_ptr<Ui::DividerLabel>(
inner,
std::move(dividerContent),
st::statisticsLimitsDividerPadding));
}
FillOverview(inner, status);
Ui::AddSkip(inner);
Ui::AddDivider(inner);
Ui::AddSkip(inner);
if (!status.prepaidGiveaway.empty()) {
const auto multiplier = Api::PremiumGiftCodeOptions(_peer)
.giveawayBoostsPerPremium();
Ui::AddSkip(inner);
AddHeader(inner, tr::lng_boosts_prepaid_giveaway_title);
Ui::AddSkip(inner);
for (const auto &g : status.prepaidGiveaway) {
using namespace Giveaway;
const auto button = inner->add(object_ptr<GiveawayTypeRow>(
inner,
g.credits
? GiveawayTypeRow::Type::PrepaidCredits
: GiveawayTypeRow::Type::Prepaid,
g.credits ? st::colorIndexOrange : g.id,
g.credits
? tr::lng_boosts_prepaid_giveaway_single()
: tr::lng_boosts_prepaid_giveaway_quantity(
lt_count,
rpl::single(g.quantity) | tr::to_count()),
g.credits
? tr::lng_boosts_prepaid_giveaway_credits_status(
lt_count,
rpl::single(g.quantity) | tr::to_count(),
lt_amount,
tr::lng_prize_credits_amount(
lt_count_decimal,
rpl::single(g.credits) | tr::to_count()))
: tr::lng_boosts_prepaid_giveaway_moths(
lt_count,
rpl::single(g.months) | tr::to_count()),
Info::Statistics::CreateBadge(
st::statisticsDetailsBottomCaptionStyle,
QString::number(
g.boosts ? g.boosts : (g.quantity * multiplier)),
st::boostsListBadgeHeight,
st::boostsListBadgeTextPadding,
st::premiumButtonBg2,
st::premiumButtonFg,
1.,
st::boostsListMiniIconPadding,
st::boostsListMiniIcon)));
button->setClickedCallback([=] {
_controller->uiShow()->showBox(Box(
CreateGiveawayBox,
_controller,
_peer,
reloadOnDone,
g));
});
}
Ui::AddSkip(inner);
Ui::AddDividerText(
inner,
tr::lng_boosts_prepaid_giveaway_title_subtext());
Ui::AddSkip(inner);
}
const auto hasBoosts = (status.firstSliceBoosts.multipliedTotal > 0);
const auto hasGifts = (status.firstSliceGifts.multipliedTotal > 0);
if (hasBoosts || hasGifts) {
auto boostClicked = [=](const Data::Boost &boost) {
if (!boost.giftCodeLink.slug.isEmpty()) {
ResolveGiftCode(_controller, boost.giftCodeLink.slug);
} else if (boost.userId) {
const auto user = _peer->owner().user(boost.userId);
if (boost.isGift || boost.isGiveaway) {
const auto d = Api::GiftCode{
.from = _peer->id,
.to = user->id,
.date = TimeId(boost.date.toSecsSinceEpoch()),
.days = boost.expiresAfterMonths * 30,
};
_show->showBox(Box(GiftCodePendingBox, _controller, d));
} else {
crl::on_main(this, [=] {
_controller->showPeerInfo(user);
});
}
} else if (boost.credits) {
_show->showBox(
Box(
::Settings::BoostCreditsBox,
_controller->parentController(),
boost));
} else if (!boost.isUnclaimed) {
_show->showToast(tr::lng_boosts_list_pending_about(tr::now));
}
};
#ifdef _DEBUG
const auto hasOneTab = false;
#else
const auto hasOneTab = (hasBoosts != hasGifts);
#endif
const auto boostsTabText = tr::lng_giveaway_quantity(
tr::now,
lt_count,
status.firstSliceBoosts.multipliedTotal);
const auto giftsTabText = tr::lng_boosts_list_tab_gifts(
tr::now,
lt_count,
status.firstSliceGifts.multipliedTotal);
if (hasOneTab) {
Ui::AddSkip(inner);
const auto header = inner->add(
object_ptr<Statistic::Header>(inner),
st::statisticsLayerMargins
+ st::boostsChartHeaderPadding);
header->resizeToWidth(header->width());
header->setTitle(hasBoosts ? boostsTabText : giftsTabText);
header->setSubTitle({});
}
const auto slider = inner->add(
object_ptr<Ui::SlideWrap<Ui::CustomWidthSlider>>(
inner,
object_ptr<Ui::CustomWidthSlider>(
inner,
st::dialogsSearchTabs)));
if (!hasOneTab) {
const auto shadow = Ui::CreateChild<Ui::PlainShadow>(inner);
shadow->show();
slider->geometryValue(
) | rpl::on_next([=](const QRect &r) {
shadow->setGeometry(
inner->x(),
rect::bottom(r) - shadow->height(),
inner->width(),
shadow->height());
}, shadow->lifetime());
}
slider->toggle(!hasOneTab, anim::type::instant);
slider->entity()->addSection(boostsTabText);
slider->entity()->addSection(giftsTabText);
{
const auto &st = st::defaultTabsSlider;
slider->entity()->setNaturalWidth(0
+ st.labelStyle.font->width(boostsTabText)
+ st.labelStyle.font->width(giftsTabText)
+ rect::m::sum::h(st::boxRowPadding));
}
const auto boostsWrap = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
const auto giftsWrap = inner->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
inner,
object_ptr<Ui::VerticalLayout>(inner)));
rpl::single(hasOneTab ? (hasGifts ? 1 : 0) : 0) | rpl::then(
slider->entity()->sectionActivated()
) | rpl::on_next([=](int index) {
boostsWrap->toggle(!index, anim::type::instant);
giftsWrap->toggle(index, anim::type::instant);
}, inner->lifetime());
Statistics::AddBoostsList(
status.firstSliceBoosts,
boostsWrap->entity(),
boostClicked,
_peer,
tr::lng_boosts_title());
Statistics::AddBoostsList(
status.firstSliceGifts,
giftsWrap->entity(),
std::move(boostClicked),
_peer,
tr::lng_boosts_title());
Ui::AddSkip(inner);
Ui::AddSkip(inner);
Ui::AddDividerText(inner, status.overview.group
? tr::lng_boosts_list_subtext_group()
: tr::lng_boosts_list_subtext());
}
Ui::AddSkip(inner);
Ui::AddSkip(inner);
AddHeader(inner, tr::lng_boosts_link_title);
Ui::AddSkip(inner, st::boostsLinkSkip);
FillShareLink(inner, _show, status.link, _peer);
Ui::AddSkip(inner);
Ui::AddDividerText(inner, status.overview.group
? tr::lng_boosts_link_subtext_group()
: tr::lng_boosts_link_subtext());
FillGetBoostsButton(inner, _controller, _show, _peer, reloadOnDone);
resizeToWidth(width());
crl::on_main(this, [=]{ fakeShowed->fire({}); });
}
void InnerWidget::saveState(not_null<Memento*> memento) {
memento->setState(base::take(_state));
}
void InnerWidget::restoreState(not_null<Memento*> memento) {
_state = memento->state();
if (!_state.link.isEmpty()) {
fill();
} else {
load();
}
Ui::RpWidget::resizeToWidth(width());
}
rpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {
return _scrollToRequests.events();
}
auto InnerWidget::showRequests() const -> rpl::producer<ShowRequest> {
return _showRequests.events();
}
void InnerWidget::showFinished() {
_showFinished.fire({});
}
not_null<PeerData*> InnerWidget::peer() const {
return _peer;
}
} // namespace Info::Boosts

View File

@@ -0,0 +1,63 @@
/*
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 "data/data_boosts.h"
#include "ui/widgets/scroll_area.h"
#include "ui/wrap/vertical_layout.h"
namespace Ui {
class Show;
} // namespace Ui
namespace Info {
class Controller;
} // namespace Info
namespace Info::Boosts {
class Memento;
class InnerWidget final : public Ui::VerticalLayout {
public:
struct ShowRequest final {
};
InnerWidget(
QWidget *parent,
not_null<Controller*> controller,
not_null<PeerData*> peer);
[[nodiscard]] not_null<PeerData*> peer() const;
[[nodiscard]] rpl::producer<Ui::ScrollToRequest> scrollToRequests() const;
[[nodiscard]] rpl::producer<ShowRequest> showRequests() const;
void showFinished();
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
private:
void load();
void fill();
not_null<Controller*> _controller;
not_null<PeerData*> _peer;
std::shared_ptr<Ui::Show> _show;
Data::BoostStatus _state;
rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
rpl::event_stream<ShowRequest> _showRequests;
rpl::event_stream<> _showFinished;
rpl::event_stream<bool> _loaded;
};
} // namespace Info::Boosts

View File

@@ -0,0 +1,118 @@
/*
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 "info/channel_statistics/boosts/info_boosts_widget.h"
#include "info/channel_statistics/boosts/info_boosts_inner_widget.h"
#include "info/info_controller.h"
#include "info/info_memento.h"
#include "lang/lang_keys.h"
#include "ui/ui_utility.h"
namespace Info::Boosts {
Memento::Memento(not_null<Controller*> controller)
: ContentMemento(controller->statisticsTag()) {
}
Memento::Memento(not_null<PeerData*> peer)
: ContentMemento(Info::Statistics::Tag{ peer, {}, {} }) {
}
Memento::~Memento() = default;
Section Memento::section() const {
return Section(Section::Type::Boosts);
}
void Memento::setState(SavedState state) {
_state = std::move(state);
}
Memento::SavedState Memento::state() {
return base::take(_state);
}
object_ptr<ContentWidget> Memento::createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) {
auto result = object_ptr<Widget>(parent, controller);
result->setInternalState(geometry, this);
return result;
}
Widget::Widget(
QWidget *parent,
not_null<Controller*> controller)
: ContentWidget(parent, controller)
, _inner(setInnerWidget(
object_ptr<InnerWidget>(
this,
controller,
controller->statisticsTag().peer))) {
_inner->showRequests(
) | rpl::on_next([=](InnerWidget::ShowRequest request) {
}, _inner->lifetime());
_inner->scrollToRequests(
) | rpl::on_next([=](const Ui::ScrollToRequest &request) {
scrollTo(request);
}, _inner->lifetime());
}
not_null<PeerData*> Widget::peer() const {
return _inner->peer();
}
bool Widget::showInternal(not_null<ContentMemento*> memento) {
return (memento->statisticsTag().peer == peer());
}
rpl::producer<QString> Widget::title() {
return tr::lng_boosts_title();
}
void Widget::setInternalState(
const QRect &geometry,
not_null<Memento*> memento) {
setGeometry(geometry);
Ui::SendPendingMoveResizeEvents(this);
restoreState(memento);
}
rpl::producer<bool> Widget::desiredShadowVisibility() const {
return rpl::single<bool>(true);
}
void Widget::showFinished() {
_inner->showFinished();
}
std::shared_ptr<ContentMemento> Widget::doCreateMemento() {
auto result = std::make_shared<Memento>(controller());
saveState(result.get());
return result;
}
void Widget::saveState(not_null<Memento*> memento) {
memento->setScrollTop(scrollTopSave());
_inner->saveState(memento);
}
void Widget::restoreState(not_null<Memento*> memento) {
_inner->restoreState(memento);
scrollTopRestore(memento->scrollTop());
}
std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer) {
return std::make_shared<Info::Memento>(
std::vector<std::shared_ptr<ContentMemento>>(
1,
std::make_shared<Memento>(peer)));
}
} // namespace Info::Boosts

View File

@@ -0,0 +1,67 @@
/*
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 "data/data_boosts.h"
#include "info/info_content_widget.h"
namespace Info::Boosts {
class InnerWidget;
class Memento final : public ContentMemento {
public:
Memento(not_null<Controller*> controller);
Memento(not_null<PeerData*> peer);
~Memento();
object_ptr<ContentWidget> createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) override;
Section section() const override;
using SavedState = Data::BoostStatus;
void setState(SavedState states);
[[nodiscard]] SavedState state();
private:
SavedState _state;
};
class Widget final : public ContentWidget {
public:
Widget(QWidget *parent, not_null<Controller*> controller);
bool showInternal(not_null<ContentMemento*> memento) override;
rpl::producer<QString> title() override;
rpl::producer<bool> desiredShadowVisibility() const override;
void showFinished() override;
[[nodiscard]] not_null<PeerData*> peer() const;
void setInternalState(
const QRect &geometry,
not_null<Memento*> memento);
private:
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
std::shared_ptr<ContentMemento> doCreateMemento() override;
const not_null<InnerWidget*> _inner;
};
[[nodiscard]] std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer);
} // namespace Info::Boosts

View File

@@ -0,0 +1,155 @@
/*
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
*/
using "ui/basic.style";
using "boxes/boxes.style";
using "statistics/statistics.style";
channelEarnLearnArrowMargins: margins(-2px, 5px, 0px, 0px);
channelEarnOverviewTitleSkip: 11px;
channelEarnOverviewMajorLabel: FlatLabel(defaultFlatLabel) {
maxHeight: 30px;
style: TextStyle(defaultTextStyle) {
font: font(15px semibold);
}
}
channelEarnOverviewMinorLabel: FlatLabel(channelEarnOverviewMajorLabel) {
style: TextStyle(defaultTextStyle) {
font: font(12px semibold);
}
}
channelEarnOverviewMinorLabelSkip: 3px;
channelEarnOverviewSubMinorLabel: FlatLabel(channelEarnOverviewMinorLabel) {
textFg: windowSubTextFg;
style: TextStyle(defaultTextStyle) {
font: font(13px);
}
}
channelEarnOverviewSubMinorLabelPos: point(4px, 2px);
channelEarnSemiboldLabel: FlatLabel(channelEarnOverviewMajorLabel) {
style: semiboldTextStyle;
}
channelEarnHeaderLabel: FlatLabel(channelEarnOverviewMajorLabel) {
style: TextStyle(statisticsHeaderTitleTextStyle) {
}
textFg: windowActiveTextFg;
}
channelEarnHistorySubLabel: FlatLabel(channelEarnOverviewSubMinorLabel) {
style: TextStyle(defaultTextStyle) {
font: font(12px);
}
}
channelEarnHistoryRecipientLabel: FlatLabel(channelEarnOverviewSubMinorLabel) {
maxHeight: 0px;
minWidth: 100px;
style: TextStyle(defaultTextStyle) {
font: font(12px);
}
}
channelEarnHistoryMajorLabel: FlatLabel(channelEarnOverviewMajorLabel) {
style: TextStyle(defaultTextStyle) {
font: font(14px semibold);
}
}
channelEarnHistoryMinorLabel: FlatLabel(channelEarnOverviewMinorLabel) {
style: TextStyle(defaultTextStyle) {
font: font(12px semibold);
}
}
channelEarnHistoryDescriptionLabel: FlatLabel(channelEarnHistoryMajorLabel) {
// boxWidth - boxRowPadding = 320 - 24 * 2
minWidth: 272px;
maxHeight: 0px;
align: align(center);
}
channelEarnHistoryMinorLabelSkip: 2px;
channelEarnHistoryOuter: margins(0px, 6px, 0px, 6px);
channelEarnHistoryTwoSkip: 5px;
channelEarnHistoryThreeSkip: 3px;
channelEarnHistoryRecipientButton: RoundButton {
textFg: transparent;
textFgOver: transparent;
numbersTextFg: transparent;
numbersTextFgOver: transparent;
textBg: windowBgOver;
textBgOver: windowBgOver;
numbersSkip: 7px;
width: 190px;
height: 34px;
padding: margins(0px, 0px, 0px, 0px);
textTop: 8px;
radius: 8px;
iconPosition: point(0px, 0px);
style: semiboldTextStyle;
ripple: RippleAnimation(defaultRippleAnimation) {
color: windowBgRipple;
}
}
channelEarnHistoryRecipientButtonLabel: FlatLabel(channelEarnHistoryRecipientLabel) {
align: align(center);
}
channelEarnBalanceMajorLabel: FlatLabel(channelEarnOverviewMajorLabel) {
style: TextStyle(defaultTextStyle) {
font: font(22px semibold);
}
}
channelEarnBalanceMinorLabel: FlatLabel(channelEarnOverviewMinorLabel) {
style: TextStyle(defaultTextStyle) {
font: font(16px semibold);
}
}
channelEarnBalanceMinorLabelSkip: 6px;
channelEarnFadeDuration: 60;
channelEarnLearnDescription: FlatLabel(defaultFlatLabel) {
maxHeight: 0px;
minWidth: 264px;
align: align(top);
}
channelEarnCurrencyCommonMargins: margins(0px, 3px, 1px, 0px);
channelEarnCurrencyLearnMargins: margins(0px, 2px, 0px, 0px);
sponsoredAboutTitleIcon: icon {{ "sponsored/large_about", activeButtonFg }};
sponsoredAboutPrivacyIcon: icon {{ "sponsored/privacy_about", boxTextFg }};
sponsoredAboutRemoveIcon: icon {{ "sponsored/remove_about", boxTextFg }};
sponsoredAboutSplitIcon: icon {{ "sponsored/revenue_split", boxTextFg }};
channelEarnLearnTitleIcon: icon {{ "sponsored/large_earn", activeButtonFg }};
channelEarnLearnChannelIcon: icon {{ "sponsored/channel", boxTextFg }};
channelEarnLearnWithdrawalsIcon: icon {{ "sponsored/withdrawals", boxTextFg }};
sponsoredReportLabel: FlatLabel(defaultFlatLabel) {
style: boxTextStyle;
minWidth: 150px;
}
botEarnInputField: InputField(defaultInputField) {
textMargins: margins(23px, 28px, 0px, 4px);
placeholderMargins: margins(-23px, 0px, 0px, 0px);
width: 100px;
heightMax: 55px;
}
botEarnLockedButtonLabel: FlatLabel(channelEarnOverviewMajorLabel) {
style: TextStyle(defaultTextStyle) {
font: font(10px semibold);
}
}
botEarnButtonLock: IconEmoji {
icon: icon{{ "chat/mini_lock", premiumButtonFg }};
padding: margins(-2px, 4px, 0px, 0px);
}

View File

@@ -0,0 +1,95 @@
/*
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 "info/channel_statistics/earn/earn_format.h"
#include <QtCore/QLocale>
namespace Info::ChannelEarn {
namespace {
constexpr auto kMinorPartLength = 9;
constexpr auto kMaxChoppedZero = kMinorPartLength - 2;
constexpr auto kZero = QChar('0');
const auto DecimalPoint = QString() + QLocale().decimalPoint();
using EarnInt = Data::EarnInt;
} // namespace
QString MajorPart(EarnInt value) {
const auto string = QString::number(value);
const auto diff = int(string.size()) - kMinorPartLength;
return (diff <= 0) ? QString(kZero) : string.mid(0, diff);
}
QString MajorPart(CreditsAmount value) {
return QString::number(int64(value.value()));
}
QString MinorPart(EarnInt value) {
if (!value) {
return DecimalPoint + kZero + kZero;
}
const auto string = QString::number(value);
const auto diff = int(string.size()) - kMinorPartLength;
const auto result = (diff < 0)
? DecimalPoint + u"%1"_q.arg(0, std::abs(diff), 10, kZero) + string
: DecimalPoint + string.mid(diff);
const auto begin = (result.constData());
const auto end = (begin + result.size());
auto ch = end - 1;
auto zeroCount = 0;
while (ch != begin) {
if (((*ch) == kZero) && (zeroCount < kMaxChoppedZero)) {
zeroCount++;
} else {
break;
}
ch--;
}
return result.chopped(zeroCount);
}
QString MinorPart(CreditsAmount value) {
static const int DecimalPointLength = DecimalPoint.length();
const auto fractional = std::abs(int(value.value() * 100)) % 100;
auto result = QString(DecimalPointLength + 2, Qt::Uninitialized);
for (int i = 0; i < DecimalPointLength; ++i) {
result[i] = DecimalPoint[i];
}
result[DecimalPointLength] = QChar('0' + fractional / 10);
result[DecimalPointLength + 1] = QChar('0' + fractional % 10);
return result;
}
QString ToUsd(
Data::EarnInt value,
float64 rate,
int afterFloat) {
return ToUsd(CreditsAmount(value), rate, afterFloat);
}
QString ToUsd(
CreditsAmount value,
float64 rate,
int afterFloat) {
constexpr auto kApproximately = QChar(0x2248);
return QString(kApproximately)
+ QChar('$')
+ QLocale().toString(
value.value() * rate,
'f',
afterFloat ? afterFloat : 2);
}
} // namespace Info::ChannelEarn

View File

@@ -0,0 +1,27 @@
/*
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 "data/data_channel_earn.h"
namespace Info::ChannelEarn {
[[nodiscard]] QString MajorPart(Data::EarnInt value);
[[nodiscard]] QString MajorPart(CreditsAmount value);
[[nodiscard]] QString MinorPart(Data::EarnInt value);
[[nodiscard]] QString MinorPart(CreditsAmount value);
[[nodiscard]] QString ToUsd(
Data::EarnInt value,
float64 rate,
int afterFloat);
[[nodiscard]] QString ToUsd(
CreditsAmount value,
float64 rate,
int afterFloat);
} // namespace Info::ChannelEarn

View File

@@ -0,0 +1,186 @@
/*
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 "info/channel_statistics/earn/earn_icons.h"
#include "ui/effects/credits_graphics.h"
#include "ui/effects/premium_graphics.h"
#include "ui/text/custom_emoji_instance.h"
#include "ui/rect.h"
#include "styles/style_credits.h"
#include "styles/style_menu_icons.h"
#include "styles/style_widgets.h"
#include "styles/style_info.h" // infoIconReport.
#include <QFile>
#include <QtSvg/QSvgRenderer>
namespace Ui::Earn {
namespace {
[[nodiscard]] QByteArray CurrencySvg(const QColor &c) {
const auto color = u"rgb(%1,%2,%3)"_q
.arg(c.red())
.arg(c.green())
.arg(c.blue())
.toUtf8();
return R"(
<svg width="72px" height="72px" viewBox="0 0 72 72">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(9.000000, 14.000000)
" stroke-width="7.2" stroke=")" + color + R"(">
<path d="M2.96014341,0 L50.9898193,0 C51.9732032,-7.06402744e-15
52.7703933,0.797190129 52.7703933,1.78057399 C52.7703933,2.08038611
52.6946886,2.3753442 52.5502994,2.63809702 L29.699977,44.2200383
C28.7527832,45.9436969 26.5876295,46.5731461 24.8639708,45.6259523
C24.2556953,45.2916896 23.7583564,44.7869606 23.4331014,44.1738213
L1.38718565,2.61498853 C0.926351231,1.74626794 1.25700829,0.668450654
2.12572888,0.20761623 C2.38272962,0.0712838007 2.6692209,4.97530809e-16
2.96014341,0 Z"></path>
<line x1="27" y1="44.4532875" x2="27" y2="0"></line>
</g>
</g>
</svg>)";
}
} // namespace
QImage IconCurrencyColored(int size, const QColor &c) {
const auto s = Size(size);
auto svg = QSvgRenderer(CurrencySvg(c));
auto image = QImage(
s * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
image.setDevicePixelRatio(style::DevicePixelRatio());
image.fill(Qt::transparent);
{
auto p = QPainter(&image);
svg.render(&p, Rect(s));
}
return image;
}
QImage IconCurrencyColored(
const style::font &font,
const QColor &c) {
return IconCurrencyColored(font->ascent, c);
}
QByteArray CurrencySvgColored(const QColor &c) {
return CurrencySvg(c);
}
QImage MenuIconCurrency(const QSize &size) {
auto image = QImage(
size * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
image.setDevicePixelRatio(style::DevicePixelRatio());
image.fill(Qt::transparent);
auto p = QPainter(&image);
st::infoIconReport.paintInCenter(
p,
Rect(size),
st::infoIconFg->c);
p.setCompositionMode(QPainter::CompositionMode_Clear);
const auto w = st::lineWidth * 6;
p.fillRect(
QRect(
rect::center(Rect(size)).x() - w / 2,
rect::center(Rect(size)).y() - w,
w,
w * 2),
Qt::white);
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
const auto s = Size(st::inviteLinkSubscribeBoxTerms.style.font->ascent);
auto svg = QSvgRenderer(CurrencySvg(st::infoIconFg->c));
svg.render(
&p,
QRectF(
(size.width() - s.width()) / 2.,
(size.height() - s.height()) / 2.,
s.width(),
s.height()));
return image;
}
QImage MenuIconCredits() {
constexpr auto kStrokeWidth = 5;
const auto sizeShift = st::lineWidth * 1.5;
auto colorized = [&] {
auto f = QFile(Ui::Premium::Svg());
if (!f.open(QIODevice::ReadOnly)) {
return QString();
}
return QString::fromUtf8(f.readAll()).replace(
u"#fff"_q,
u"#ffffff00"_q);
}();
colorized.replace(
u"stroke=\"none\""_q,
u"stroke=\"%1\""_q.arg(st::menuIconColor->c.name()));
colorized.replace(
u"stroke-width=\"1\""_q,
u"stroke-width=\"%1\""_q.arg(kStrokeWidth));
auto svg = QSvgRenderer(colorized.toUtf8());
svg.setViewBox(svg.viewBox()
+ Margins(style::ConvertScale(kStrokeWidth)));
auto image = QImage(
st::menuIconLinks.size() * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
image.setDevicePixelRatio(style::DevicePixelRatio());
image.fill(Qt::transparent);
{
auto p = QPainter(&image);
svg.render(&p, Rect(st::menuIconLinks.size()) - Margins(sizeShift));
}
return image;
}
std::unique_ptr<Ui::Text::CustomEmoji> MakeCurrencyIconEmoji(
const style::font &font,
const QColor &c) {
return std::make_unique<Ui::CustomEmoji::Internal>(
u"currency_icon:%1:%2"_q.arg(font->height).arg(c.name()),
IconCurrencyColored(font, c));
}
Ui::Text::PaletteDependentEmoji IconCreditsEmoji(
IconDescriptor descriptor) {
return { .factory = [=] {
return Ui::GenerateStars(
(descriptor.size
? descriptor.size
: st::defaultTableLabel.style.font->height),
1);
}, .margin = descriptor.margin.value_or(
QMargins{ 0, st::giftBoxByStarsSkip, 0, 0 }) };
}
Ui::Text::PaletteDependentEmoji IconCurrencyEmoji(
IconDescriptor descriptor) {
return { .factory = [=] {
return IconCurrencyColored(
descriptor.size ? descriptor.size : st::earnTonIconSize,
st::currencyFg->c);
}, .margin = descriptor.margin.value_or(st::earnTonIconMargin) };
}
Ui::Text::PaletteDependentEmoji IconCreditsEmojiSmall() {
return IconCreditsEmoji({
.size = st::giftBoxByStarsStyle.font->height,
.margin = QMargins{ 0, st::giftBoxByStarsStarTop, 0, 0 },
});
}
Ui::Text::PaletteDependentEmoji IconCurrencyEmojiSmall() {
return IconCreditsEmoji({});
}
} // namespace Ui::Earn

View File

@@ -0,0 +1,43 @@
/*
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/custom_emoji_helper.h"
namespace Ui::Text {
class CustomEmoji;
} // namespace Ui::Text
namespace Ui::Earn {
[[nodiscard]] QImage IconCurrencyColored(int size, const QColor &c);
[[nodiscard]] QImage IconCurrencyColored(
const style::font &font,
const QColor &c);
[[nodiscard]] QByteArray CurrencySvgColored(const QColor &c);
[[nodiscard]] QImage MenuIconCurrency(const QSize &size);
[[nodiscard]] QImage MenuIconCredits();
std::unique_ptr<Ui::Text::CustomEmoji> MakeCurrencyIconEmoji(
const style::font &font,
const QColor &c);
struct IconDescriptor {
int size = 0;
std::optional<QMargins> margin;
};
[[nodiscard]] Ui::Text::PaletteDependentEmoji IconCreditsEmoji(
IconDescriptor descriptor = {});
[[nodiscard]] Ui::Text::PaletteDependentEmoji IconCurrencyEmoji(
IconDescriptor descriptor = {});
[[nodiscard]] Ui::Text::PaletteDependentEmoji IconCreditsEmojiSmall();
[[nodiscard]] Ui::Text::PaletteDependentEmoji IconCurrencyEmojiSmall();
} // namespace Ui::Earn

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,73 @@
/*
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 "info/channel_statistics/earn/info_channel_earn_widget.h"
#include "ui/widgets/scroll_area.h"
#include "ui/wrap/vertical_layout.h"
namespace Ui {
class Show;
class FlatLabel;
} // namespace Ui
namespace Info {
class Controller;
} // namespace Info
namespace Info::ChannelEarn {
class Memento;
class InnerWidget final : public Ui::VerticalLayout {
public:
struct ShowRequest final {
};
InnerWidget(
QWidget *parent,
not_null<Controller*> controller,
not_null<PeerData*> peer);
[[nodiscard]] not_null<PeerData*> peer() const;
[[nodiscard]] rpl::producer<Ui::ScrollToRequest> scrollToRequests() const;
[[nodiscard]] rpl::producer<ShowRequest> showRequests() const;
void showFinished();
void setInnerFocus();
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
private:
void load();
void fill();
not_null<Controller*> _controller;
not_null<PeerData*> _peer;
std::shared_ptr<Ui::Show> _show;
Memento::SavedState _state;
rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
rpl::event_stream<ShowRequest> _showRequests;
rpl::event_stream<> _showFinished;
rpl::event_stream<> _focusRequested;
rpl::event_stream<bool> _loaded;
rpl::event_stream<> _stateUpdated;
};
void AddEmojiToMajor(
not_null<Ui::FlatLabel*> label,
rpl::producer<CreditsAmount> value,
std::optional<bool> isIn,
std::optional<QMargins> margins);
} // namespace Info::ChannelEarn

View File

@@ -0,0 +1,122 @@
/*
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 "info/channel_statistics/earn/info_channel_earn_widget.h"
#include "info/channel_statistics/earn/info_channel_earn_list.h"
#include "info/info_controller.h"
#include "info/info_memento.h"
#include "lang/lang_keys.h"
#include "ui/ui_utility.h"
namespace Info::ChannelEarn {
Memento::Memento(not_null<Controller*> controller)
: ContentMemento(controller->statisticsTag()) {
}
Memento::Memento(not_null<PeerData*> peer)
: ContentMemento(Info::Statistics::Tag{ peer, {}, {} }) {
}
Memento::~Memento() = default;
Section Memento::section() const {
return Section(Section::Type::ChannelEarn);
}
void Memento::setState(SavedState state) {
_state = std::move(state);
}
Memento::SavedState Memento::state() {
return base::take(_state);
}
object_ptr<ContentWidget> Memento::createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) {
auto result = object_ptr<Widget>(parent, controller);
result->setInternalState(geometry, this);
return result;
}
Widget::Widget(
QWidget *parent,
not_null<Controller*> controller)
: ContentWidget(parent, controller)
, _inner(setInnerWidget(
object_ptr<InnerWidget>(
this,
controller,
controller->statisticsTag().peer))) {
_inner->showRequests(
) | rpl::on_next([=](InnerWidget::ShowRequest request) {
}, _inner->lifetime());
_inner->scrollToRequests(
) | rpl::on_next([=](const Ui::ScrollToRequest &request) {
scrollTo(request);
}, _inner->lifetime());
}
not_null<PeerData*> Widget::peer() const {
return _inner->peer();
}
bool Widget::showInternal(not_null<ContentMemento*> memento) {
return (memento->statisticsTag().peer == peer());
}
rpl::producer<QString> Widget::title() {
return tr::lng_channel_earn_title();
}
void Widget::setInternalState(
const QRect &geometry,
not_null<Memento*> memento) {
setGeometry(geometry);
Ui::SendPendingMoveResizeEvents(this);
restoreState(memento);
}
rpl::producer<bool> Widget::desiredShadowVisibility() const {
return rpl::single<bool>(true);
}
void Widget::showFinished() {
_inner->showFinished();
}
void Widget::setInnerFocus() {
_inner->setInnerFocus();
}
std::shared_ptr<ContentMemento> Widget::doCreateMemento() {
auto result = std::make_shared<Memento>(controller());
saveState(result.get());
return result;
}
void Widget::saveState(not_null<Memento*> memento) {
memento->setScrollTop(scrollTopSave());
_inner->saveState(memento);
}
void Widget::restoreState(not_null<Memento*> memento) {
_inner->restoreState(memento);
scrollTopRestore(memento->scrollTop());
}
std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer) {
return std::make_shared<Info::Memento>(
std::vector<std::shared_ptr<ContentMemento>>(
1,
std::make_shared<Memento>(peer)));
}
} // namespace Info::ChannelEarn

View File

@@ -0,0 +1,76 @@
/*
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 "data/data_channel_earn.h"
#include "data/data_credits.h"
#include "data/data_credits_earn.h"
#include "info/info_content_widget.h"
namespace Info::ChannelEarn {
class InnerWidget;
class Memento final : public ContentMemento {
public:
Memento(not_null<Controller*> controller);
Memento(not_null<PeerData*> peer);
~Memento();
object_ptr<ContentWidget> createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) override;
Section section() const override;
struct SavedState final {
Data::EarnStatistics currencyEarn;
Data::CreditsEarnStatistics creditsEarn;
Data::CreditsStatusSlice creditsStatusSlice;
PeerId premiumBotId = PeerId(0);
bool canViewCurrencyMegagroupEarn = true;
};
void setState(SavedState states);
[[nodiscard]] SavedState state();
private:
SavedState _state;
};
class Widget final : public ContentWidget {
public:
Widget(QWidget *parent, not_null<Controller*> controller);
bool showInternal(not_null<ContentMemento*> memento) override;
rpl::producer<QString> title() override;
rpl::producer<bool> desiredShadowVisibility() const override;
void showFinished() override;
void setInnerFocus() override;
[[nodiscard]] not_null<PeerData*> peer() const;
void setInternalState(
const QRect &geometry,
not_null<Memento*> memento);
private:
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
std::shared_ptr<ContentMemento> doCreateMemento() override;
const not_null<InnerWidget*> _inner;
};
[[nodiscard]] std::shared_ptr<Info::Memento> Make(not_null<PeerData*> peer);
} // namespace Info::ChannelEarn

View File

@@ -0,0 +1,312 @@
/*
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 "info/common_groups/info_common_groups_inner_widget.h"
#include "base/weak_ptr.h"
#include "info/common_groups/info_common_groups_widget.h"
#include "info/info_controller.h"
#include "lang/lang_keys.h"
#include "mtproto/sender.h"
#include "main/main_session.h"
#include "window/window_session_controller.h"
#include "ui/widgets/scroll_area.h"
#include "ui/search_field_controller.h"
#include "data/data_user.h"
#include "data/data_session.h"
#include "styles/style_info.h"
#include "styles/style_widgets.h"
namespace Info {
namespace CommonGroups {
namespace {
constexpr auto kCommonGroupsPerPage = 40;
constexpr auto kCommonGroupsSearchAfter = 20;
class ListController final
: public PeerListController
, public base::has_weak_ptr {
public:
ListController(
not_null<Controller*> controller,
not_null<UserData*> user);
Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
void loadMoreRows() override;
std::unique_ptr<PeerListRow> createRestoredRow(
not_null<PeerData*> peer) override {
return createRow(peer);
}
std::unique_ptr<PeerListState> saveState() const override;
void restoreState(std::unique_ptr<PeerListState> state) override;
private:
std::unique_ptr<PeerListRow> createRow(not_null<PeerData*> peer);
struct SavedState : SavedStateBase {
PeerId preloadGroupId = 0;
bool allLoaded = false;
bool wasLoading = false;
};
const not_null<Controller*> _controller;
MTP::Sender _api;
not_null<UserData*> _user;
mtpRequestId _preloadRequestId = 0;
bool _allLoaded = false;
PeerId _preloadGroupId = 0;
};
ListController::ListController(
not_null<Controller*> controller,
not_null<UserData*> user)
: PeerListController()
, _controller(controller)
, _api(&_controller->session().mtp())
, _user(user) {
_controller->setSearchEnabledByContent(false);
}
Main::Session &ListController::session() const {
return _user->session();
}
std::unique_ptr<PeerListRow> ListController::createRow(
not_null<PeerData*> peer) {
auto result = std::make_unique<PeerListRow>(peer);
result->setCustomStatus(QString());
return result;
}
void ListController::prepare() {
setSearchNoResultsText(tr::lng_bot_groups_not_found(tr::now));
delegate()->peerListSetSearchMode(PeerListSearchMode::Enabled);
delegate()->peerListSetTitle(tr::lng_profile_common_groups_section());
}
void ListController::loadMoreRows() {
if (_preloadRequestId || _allLoaded) {
return;
}
_preloadRequestId = _api.request(MTPmessages_GetCommonChats(
_user->inputUser(),
MTP_long(peerIsChat(_preloadGroupId)
? peerToChat(_preloadGroupId).bare
: peerToChannel(_preloadGroupId).bare),
MTP_int(kCommonGroupsPerPage)
)).done([this](const MTPmessages_Chats &result) {
_preloadRequestId = 0;
_preloadGroupId = 0;
_allLoaded = true;
const auto &chats = result.match([](const auto &data) {
return data.vchats().v;
});
if (!chats.empty()) {
auto add = std::vector<not_null<PeerData*>>();
auto allLoaded = _allLoaded;
auto preloadGroupId = _preloadGroupId;
const auto owner = &_user->owner();
const auto weak = base::make_weak(this);
for (const auto &chat : chats) {
if (const auto peer = owner->processChat(chat)) {
if (!peer->migrateTo()) {
add.push_back(peer);
}
preloadGroupId = peer->id;
allLoaded = false;
}
}
if (!weak) {
return;
}
for (const auto &peer : add) {
if (!delegate()->peerListFindRow(peer->id.value)) {
delegate()->peerListAppendRow(createRow(peer));
}
}
_preloadGroupId = preloadGroupId;
_allLoaded = allLoaded;
delegate()->peerListRefreshRows();
}
auto fullCount = delegate()->peerListFullRowsCount();
if (fullCount > kCommonGroupsSearchAfter) {
_controller->setSearchEnabledByContent(true);
}
}).send();
}
std::unique_ptr<PeerListState> ListController::saveState() const {
auto result = PeerListController::saveState();
auto my = std::make_unique<SavedState>();
my->preloadGroupId = _preloadGroupId;
my->allLoaded = _allLoaded;
my->wasLoading = (_preloadRequestId != 0);
result->controllerState = std::move(my);
return result;
}
void ListController::restoreState(
std::unique_ptr<PeerListState> state) {
auto typeErasedState = state
? state->controllerState.get()
: nullptr;
if (auto my = dynamic_cast<SavedState*>(typeErasedState)) {
if (auto requestId = base::take(_preloadRequestId)) {
_api.request(requestId).cancel();
}
_allLoaded = my->allLoaded;
_preloadGroupId = my->preloadGroupId;
if (my->wasLoading) {
loadMoreRows();
}
PeerListController::restoreState(std::move(state));
auto fullCount = delegate()->peerListFullRowsCount();
if (fullCount > kCommonGroupsSearchAfter) {
_controller->setSearchEnabledByContent(true);
}
}
}
void ListController::rowClicked(not_null<PeerListRow*> row) {
const auto peer = row->peer();
const auto controller = _controller->parentController();
if (const auto forum = peer->forum()) {
controller->showForum(forum);
} else {
controller->showPeerHistory(
peer,
Window::SectionShow::Way::Forward);
}
}
} // namespace
InnerWidget::InnerWidget(
QWidget *parent,
not_null<Controller*> controller,
not_null<UserData*> user)
: RpWidget(parent)
, _show(controller->uiShow())
, _controller(controller)
, _user(user)
, _listController(std::make_unique<ListController>(controller, _user))
, _list(setupList(this, _listController.get())) {
setContent(_list.data());
_listController->setDelegate(static_cast<PeerListDelegate*>(this));
_controller->searchFieldController()->queryValue(
) | rpl::on_next([this](QString &&query) {
peerListScrollToTop();
content()->searchQueryChanged(std::move(query));
}, lifetime());
}
void InnerWidget::visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) {
setChildVisibleTopBottom(_list, visibleTop, visibleBottom);
}
void InnerWidget::saveState(not_null<Memento*> memento) {
memento->setListState(_listController->saveState());
}
void InnerWidget::restoreState(not_null<Memento*> memento) {
_listController->restoreState(memento->listState());
}
rpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {
return _scrollToRequests.events();
}
int InnerWidget::desiredHeight() const {
auto desired = 0;
auto count = qMax(_user->commonChatsCount(), 1);
desired += qMax(count, _list->fullRowsCount())
* st::infoCommonGroupsList.item.height;
return qMax(height(), desired);
}
object_ptr<InnerWidget::ListWidget> InnerWidget::setupList(
RpWidget *parent,
not_null<PeerListController*> controller) const {
controller->setStyleOverrides(&st::infoCommonGroupsList);
auto result = object_ptr<ListWidget>(
parent,
controller);
result->scrollToRequests(
) | rpl::on_next([this](Ui::ScrollToRequest request) {
auto addmin = (request.ymin < 0)
? 0
: st::infoCommonGroupsMargin.top();
auto addmax = (request.ymax < 0)
? 0
: st::infoCommonGroupsMargin.top();
_scrollToRequests.fire({
request.ymin + addmin,
request.ymax + addmax });
}, result->lifetime());
result->moveToLeft(0, st::infoCommonGroupsMargin.top());
parent->widthValue(
) | rpl::on_next([list = result.data()](int newWidth) {
list->resizeToWidth(newWidth);
}, result->lifetime());
result->heightValue(
) | rpl::on_next([parent](int listHeight) {
auto newHeight = st::infoCommonGroupsMargin.top()
+ listHeight
+ st::infoCommonGroupsMargin.bottom();
parent->resize(parent->width(), newHeight);
}, result->lifetime());
return result;
}
void InnerWidget::peerListSetTitle(rpl::producer<QString> title) {
}
void InnerWidget::peerListSetAdditionalTitle(rpl::producer<QString> title) {
}
bool InnerWidget::peerListIsRowChecked(not_null<PeerListRow*> row) {
return false;
}
int InnerWidget::peerListSelectedRowsCount() {
return 0;
}
void InnerWidget::peerListScrollToTop() {
_scrollToRequests.fire({ -1, -1 });
}
void InnerWidget::peerListAddSelectedPeerInBunch(not_null<PeerData*> peer) {
Unexpected("Item selection in Info::Profile::Members.");
}
void InnerWidget::peerListAddSelectedRowInBunch(not_null<PeerListRow*> row) {
Unexpected("Item selection in Info::Profile::Members.");
}
void InnerWidget::peerListFinishSelectedRowsBunch() {
}
void InnerWidget::peerListSetDescription(
object_ptr<Ui::FlatLabel> description) {
description.destroy();
}
std::shared_ptr<Main::SessionShow> InnerWidget::peerListUiShow() {
return _show;
}
} // namespace CommonGroups
} // namespace Info

View File

@@ -0,0 +1,85 @@
/*
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 <rpl/producer.h>
#include "ui/rp_widget.h"
#include "boxes/peer_list_box.h"
namespace Ui {
class Show;
} // namespace Ui
namespace Info {
class Controller;
namespace CommonGroups {
class Memento;
class InnerWidget final
: public Ui::RpWidget
, private PeerListContentDelegate {
public:
InnerWidget(
QWidget *parent,
not_null<Controller*> controller,
not_null<UserData*> user);
not_null<UserData*> user() const {
return _user;
}
rpl::producer<Ui::ScrollToRequest> scrollToRequests() const;
int desiredHeight() const;
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
protected:
void visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) override;
private:
using ListWidget = PeerListContent;
// PeerListContentDelegate interface.
void peerListSetTitle(rpl::producer<QString> title) override;
void peerListSetAdditionalTitle(rpl::producer<QString> title) override;
bool peerListIsRowChecked(not_null<PeerListRow*> row) override;
int peerListSelectedRowsCount() override;
void peerListScrollToTop() override;
void peerListAddSelectedPeerInBunch(
not_null<PeerData*> peer) override;
void peerListAddSelectedRowInBunch(
not_null<PeerListRow*> row) override;
void peerListFinishSelectedRowsBunch() override;
void peerListSetDescription(
object_ptr<Ui::FlatLabel> description) override;
std::shared_ptr<Main::SessionShow> peerListUiShow() override;
object_ptr<ListWidget> setupList(
RpWidget *parent,
not_null<PeerListController*> controller) const;
std::shared_ptr<Main::SessionShow> _show;
not_null<Controller*> _controller;
not_null<UserData*> _user;
std::unique_ptr<PeerListController> _listController;
object_ptr<ListWidget> _list;
rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
};
} // namespace CommonGroups
} // namespace Info

View File

@@ -0,0 +1,112 @@
/*
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 "info/common_groups/info_common_groups_widget.h"
#include "info/common_groups/info_common_groups_inner_widget.h"
#include "info/info_controller.h"
#include "ui/widgets/scroll_area.h"
#include "ui/ui_utility.h"
#include "lang/lang_keys.h"
#include "data/data_user.h"
#include "data/data_session.h"
#include "main/main_session.h"
#include "styles/style_info.h"
namespace Info {
namespace CommonGroups {
Memento::Memento(not_null<UserData*> user)
: ContentMemento(user, nullptr, nullptr, PeerId()) {
}
Section Memento::section() const {
return Section(Section::Type::CommonGroups);
}
not_null<UserData*> Memento::user() const {
return peer()->asUser();
}
object_ptr<ContentWidget> Memento::createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) {
auto result = object_ptr<Widget>(parent, controller, user());
result->setInternalState(geometry, this);
return result;
}
void Memento::setListState(std::unique_ptr<PeerListState> state) {
_listState = std::move(state);
}
std::unique_ptr<PeerListState> Memento::listState() {
return std::move(_listState);
}
Memento::~Memento() = default;
Widget::Widget(
QWidget *parent,
not_null<Controller*> controller,
not_null<UserData*> user)
: ContentWidget(parent, controller) {
_inner = setInnerWidget(object_ptr<InnerWidget>(
this,
controller,
user));
}
rpl::producer<QString> Widget::title() {
return tr::lng_profile_common_groups_section();
}
not_null<UserData*> Widget::user() const {
return _inner->user();
}
bool Widget::showInternal(not_null<ContentMemento*> memento) {
if (!controller()->validateMementoPeer(memento)) {
return false;
}
if (auto groupsMemento = dynamic_cast<Memento*>(memento.get())) {
if (groupsMemento->user() == user()) {
restoreState(groupsMemento);
return true;
}
}
return false;
}
void Widget::setInternalState(
const QRect &geometry,
not_null<Memento*> memento) {
setGeometry(geometry);
Ui::SendPendingMoveResizeEvents(this);
restoreState(memento);
}
std::shared_ptr<ContentMemento> Widget::doCreateMemento() {
auto result = std::make_shared<Memento>(user());
saveState(result.get());
return result;
}
void Widget::saveState(not_null<Memento*> memento) {
memento->setScrollTop(scrollTopSave());
_inner->saveState(memento);
}
void Widget::restoreState(not_null<Memento*> memento) {
_inner->restoreState(memento);
scrollTopRestore(memento->scrollTop());
}
} // namespace CommonGroups
} // namespace Info

View File

@@ -0,0 +1,77 @@
/*
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 <rpl/producer.h>
#include "info/info_content_widget.h"
struct PeerListState;
namespace Ui {
class SearchFieldController;
} // namespace Ui
namespace Info {
namespace CommonGroups {
class InnerWidget;
class Memento final : public ContentMemento {
public:
explicit Memento(not_null<UserData*> user);
object_ptr<ContentWidget> createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) override;
Section section() const override;
not_null<UserData*> user() const;
void setListState(std::unique_ptr<PeerListState> state);
std::unique_ptr<PeerListState> listState();
~Memento();
private:
std::unique_ptr<PeerListState> _listState;
};
class Widget final : public ContentWidget {
public:
Widget(
QWidget *parent,
not_null<Controller*> controller,
not_null<UserData*> user);
not_null<UserData*> user() const;
bool showInternal(
not_null<ContentMemento*> memento) override;
void setInternalState(
const QRect &geometry,
not_null<Memento*> memento);
rpl::producer<QString> title() override;
private:
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
std::shared_ptr<ContentMemento> doCreateMemento() override;
InnerWidget *_inner = nullptr;
};
} // namespace CommonGroups
} // namespace Info

View File

@@ -0,0 +1,203 @@
/*
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 "info/downloads/info_downloads_inner_widget.h"
#include "info/downloads/info_downloads_widget.h"
#include "info/media/info_media_list_widget.h"
#include "info/info_controller.h"
#include "ui/widgets/labels.h"
#include "ui/search_field_controller.h"
#include "lang/lang_keys.h"
#include "styles/style_info.h"
namespace Info::Downloads {
class EmptyWidget : public Ui::RpWidget {
public:
EmptyWidget(QWidget *parent);
void setFullHeight(rpl::producer<int> fullHeightValue);
void setSearchQuery(const QString &query);
protected:
int resizeGetHeight(int newWidth) override;
void paintEvent(QPaintEvent *e) override;
private:
object_ptr<Ui::FlatLabel> _text;
int _height = 0;
};
EmptyWidget::EmptyWidget(QWidget *parent)
: RpWidget(parent)
, _text(this, st::infoEmptyLabel) {
}
void EmptyWidget::setFullHeight(rpl::producer<int> fullHeightValue) {
std::move(
fullHeightValue
) | rpl::on_next([this](int fullHeight) {
// Make icon center be on 1/3 height.
auto iconCenter = fullHeight / 3;
auto iconHeight = st::infoEmptyFile.height();
auto iconTop = iconCenter - iconHeight / 2;
_height = iconTop + st::infoEmptyIconTop;
resizeToWidth(width());
}, lifetime());
}
void EmptyWidget::setSearchQuery(const QString &query) {
_text->setText(query.isEmpty()
? tr::lng_media_file_empty(tr::now)
: tr::lng_media_file_empty_search(tr::now));
resizeToWidth(width());
}
int EmptyWidget::resizeGetHeight(int newWidth) {
auto labelTop = _height - st::infoEmptyLabelTop;
auto labelWidth = newWidth - 2 * st::infoEmptyLabelSkip;
_text->resizeToNaturalWidth(labelWidth);
auto labelLeft = (newWidth - _text->width()) / 2;
_text->moveToLeft(labelLeft, labelTop, newWidth);
update();
return _height;
}
void EmptyWidget::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
const auto iconLeft = (width() - st::infoEmptyFile.width()) / 2;
const auto iconTop = height() - st::infoEmptyIconTop;
st::infoEmptyFile.paint(p, iconLeft, iconTop, width());
}
InnerWidget::InnerWidget(
QWidget *parent,
not_null<Controller*> controller)
: RpWidget(parent)
, _controller(controller)
, _empty(this) {
_empty->heightValue(
) | rpl::on_next(
[this] { refreshHeight(); },
_empty->lifetime());
_list = setupList();
}
void InnerWidget::visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) {
setChildVisibleTopBottom(_list, visibleTop, visibleBottom);
}
bool InnerWidget::showInternal(not_null<Memento*> memento) {
if (memento->section().type() == Section::Type::Downloads) {
restoreState(memento);
return true;
}
return false;
}
object_ptr<Media::ListWidget> InnerWidget::setupList() {
auto result = object_ptr<Media::ListWidget>(this, _controller);
result->heightValue(
) | rpl::on_next(
[this] { refreshHeight(); },
result->lifetime());
using namespace rpl::mappers;
result->scrollToRequests(
) | rpl::map([widget = result.data()](int to) {
return Ui::ScrollToRequest {
widget->y() + to,
-1
};
}) | rpl::start_to_stream(
_scrollToRequests,
result->lifetime());
_selectedLists.fire(result->selectedListValue());
_listTops.fire(result->topValue());
_controller->searchQueryValue(
) | rpl::on_next([this](const QString &query) {
_empty->setSearchQuery(query);
}, result->lifetime());
return result;
}
void InnerWidget::saveState(not_null<Memento*> memento) {
_list->saveState(&memento->media());
}
void InnerWidget::restoreState(not_null<Memento*> memento) {
_list->restoreState(&memento->media());
}
rpl::producer<SelectedItems> InnerWidget::selectedListValue() const {
return _selectedLists.events_starting_with(
_list->selectedListValue()
) | rpl::flatten_latest();
}
void InnerWidget::selectionAction(SelectionAction action) {
_list->selectionAction(action);
}
InnerWidget::~InnerWidget() = default;
int InnerWidget::resizeGetHeight(int newWidth) {
_inResize = true;
auto guard = gsl::finally([this] { _inResize = false; });
_list->resizeToWidth(newWidth);
_empty->resizeToWidth(newWidth);
return recountHeight();
}
void InnerWidget::refreshHeight() {
if (_inResize) {
return;
}
resize(width(), recountHeight());
}
int InnerWidget::recountHeight() {
auto top = 0;
auto listHeight = 0;
if (_list) {
_list->moveToLeft(0, top);
listHeight = _list->heightNoMargins();
top += listHeight;
}
if (listHeight > 0) {
_empty->hide();
} else {
_empty->show();
_empty->moveToLeft(0, top);
top += _empty->heightNoMargins();
}
return top;
}
void InnerWidget::setScrollHeightValue(rpl::producer<int> value) {
using namespace rpl::mappers;
_empty->setFullHeight(rpl::combine(
std::move(value),
_listTops.events_starting_with(
_list->topValue()
) | rpl::flatten_latest(),
_1 - _2));
}
rpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {
return _scrollToRequests.events();
}
} // namespace Info::Downloads

View File

@@ -0,0 +1,79 @@
/*
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/rp_widget.h"
#include "ui/widgets/scroll_area.h"
#include "base/unique_qptr.h"
namespace Ui {
class VerticalLayout;
class SearchFieldController;
} // namespace Ui
namespace Info {
class Controller;
struct SelectedItems;
enum class SelectionAction;
namespace Media {
class ListWidget;
} // namespace Media
namespace Downloads {
class Memento;
class EmptyWidget;
class InnerWidget final : public Ui::RpWidget {
public:
InnerWidget(
QWidget *parent,
not_null<Controller*> controller);
bool showInternal(not_null<Memento*> memento);
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
void setScrollHeightValue(rpl::producer<int> value);
rpl::producer<Ui::ScrollToRequest> scrollToRequests() const;
rpl::producer<SelectedItems> selectedListValue() const;
void selectionAction(SelectionAction action);
~InnerWidget();
protected:
int resizeGetHeight(int newWidth) override;
void visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) override;
private:
int recountHeight();
void refreshHeight();
object_ptr<Media::ListWidget> setupList();
const not_null<Controller*> _controller;
object_ptr<Media::ListWidget> _list = { nullptr };
object_ptr<EmptyWidget> _empty;
bool _inResize = false;
rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
rpl::event_stream<rpl::producer<SelectedItems>> _selectedLists;
rpl::event_stream<rpl::producer<int>> _listTops;
};
} // namespace Downloads
} // namespace Info

View File

@@ -0,0 +1,576 @@
/*
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 "info/downloads/info_downloads_provider.h"
#include "info/media/info_media_widget.h"
#include "info/media/info_media_list_section.h"
#include "info/info_controller.h"
#include "ui/text/format_song_document_name.h"
#include "ui/ui_utility.h"
#include "data/data_download_manager.h"
#include "data/data_document.h"
#include "data/data_media_types.h"
#include "data/data_session.h"
#include "main/main_account.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "history/history_item.h"
#include "history/history_item_helpers.h"
#include "history/history.h"
#include "core/application.h"
#include "storage/storage_shared_media.h"
#include "layout/layout_selection.h"
#include "styles/style_overview.h"
namespace Info::Downloads {
namespace {
using namespace Media;
} // namespace
Provider::Provider(not_null<AbstractController*> controller)
: _controller(controller)
, _storiesAddToAlbumId(_controller->storiesAddToAlbumId()) {
style::PaletteChanged(
) | rpl::on_next([=] {
for (auto &layout : _layouts) {
layout.second.item->invalidateCache();
}
}, _lifetime);
}
Type Provider::type() {
return Type::File;
}
bool Provider::hasSelectRestriction() {
return false;
}
rpl::producer<bool> Provider::hasSelectRestrictionChanges() {
return rpl::never<bool>();
}
bool Provider::sectionHasFloatingHeader() {
return false;
}
QString Provider::sectionTitle(not_null<const BaseLayout*> item) {
return QString();
}
bool Provider::sectionItemBelongsHere(
not_null<const BaseLayout*> item,
not_null<const BaseLayout*> previous) {
return true;
}
bool Provider::isPossiblyMyItem(not_null<const HistoryItem*> item) {
return true;
}
std::optional<int> Provider::fullCount() {
return _queryWords.empty()
? _fullCount
: (_foundCount || _fullCount.has_value())
? _foundCount
: std::optional<int>();
}
void Provider::restart() {
}
void Provider::checkPreload(
QSize viewport,
not_null<BaseLayout*> topLayout,
not_null<BaseLayout*> bottomLayout,
bool preloadTop,
bool preloadBottom) {
}
void Provider::setSearchQuery(QString query) {
if (_query == query) {
return;
}
_query = query;
auto words = TextUtilities::PrepareSearchWords(_query);
if (!_started || _queryWords == words) {
return;
}
_queryWords = std::move(words);
if (searchMode()) {
_foundCount = 0;
for (auto &element : _elements) {
if ((element.found = computeIsFound(element))) {
++_foundCount;
}
}
}
_refreshed.fire({});
}
void Provider::refreshViewer() {
if (_started) {
return;
}
_started = true;
auto &manager = Core::App().downloadManager();
rpl::single(rpl::empty) | rpl::then(
manager.loadingListChanges() | rpl::to_empty
) | rpl::on_next([=, &manager] {
auto copy = _downloading;
for (const auto id : manager.loadingList()) {
if (!id->done) {
const auto item = id->object.item;
if (!copy.remove(item) && !_downloaded.contains(item)) {
_downloading.emplace(item);
addElementNow({
.item = item,
.started = id->started,
.path = id->path,
});
trackItemSession(item);
refreshPostponed(true);
}
}
}
for (const auto &item : copy) {
Assert(!_downloaded.contains(item));
remove(item);
}
if (!_fullCount.has_value()) {
refreshPostponed(false);
}
}, _lifetime);
for (const auto id : manager.loadedList()) {
addPostponed(id);
}
manager.loadedAdded(
) | rpl::on_next([=](not_null<const Data::DownloadedId*> entry) {
addPostponed(entry);
}, _lifetime);
manager.loadedRemoved(
) | rpl::on_next([=](not_null<const HistoryItem*> item) {
if (!_downloading.contains(item)) {
remove(item);
} else {
_downloaded.remove(item);
_addPostponed.erase(
ranges::remove(_addPostponed, item, &Element::item),
end(_addPostponed));
}
}, _lifetime);
manager.loadedResolveDone(
) | rpl::on_next([=] {
if (!_fullCount.has_value()) {
_fullCount = 0;
}
}, _lifetime);
performAdd();
performRefresh();
}
void Provider::addPostponed(not_null<const Data::DownloadedId*> entry) {
Expects(entry->object != nullptr);
const auto item = entry->object->item;
trackItemSession(item);
const auto i = ranges::find(_addPostponed, item, &Element::item);
if (i != end(_addPostponed)) {
i->path = entry->path;
i->started = entry->started;
} else {
_addPostponed.push_back({
.item = item,
.started = entry->started,
.path = entry->path,
});
if (_addPostponed.size() == 1) {
Ui::PostponeCall(this, [=] {
performAdd();
});
}
}
}
void Provider::performAdd() {
if (_addPostponed.empty()) {
return;
}
for (auto &element : base::take(_addPostponed)) {
_downloaded.emplace(element.item);
if (!_downloading.remove(element.item)) {
addElementNow(std::move(element));
}
}
refreshPostponed(true);
}
void Provider::addElementNow(Element &&element) {
_elements.push_back(std::move(element));
auto &added = _elements.back();
fillSearchIndex(added);
added.found = searchMode() && computeIsFound(added);
if (added.found) {
++_foundCount;
}
}
void Provider::remove(not_null<const HistoryItem*> item) {
_addPostponed.erase(
ranges::remove(_addPostponed, item, &Element::item),
end(_addPostponed));
_downloading.remove(item);
_downloaded.remove(item);
const auto proj = [&](const Element &element) {
if (element.item != item) {
return false;
} else if (element.found && searchMode()) {
--_foundCount;
}
return true;
};
_elements.erase(ranges::remove_if(_elements, proj), end(_elements));
if (const auto i = _layouts.find(item); i != end(_layouts)) {
_layoutRemoved.fire(i->second.item.get());
_layouts.erase(i);
}
refreshPostponed(false);
}
void Provider::refreshPostponed(bool added) {
if (added) {
_postponedRefreshSort = true;
}
if (!_postponedRefresh) {
_postponedRefresh = true;
Ui::PostponeCall(this, [=] {
performRefresh();
});
}
}
void Provider::performRefresh() {
if (!_postponedRefresh) {
return;
}
_postponedRefresh = false;
if (!_elements.empty() || _fullCount.has_value()) {
_fullCount = _elements.size();
}
if (base::take(_postponedRefreshSort)) {
ranges::sort(_elements, ranges::less(), &Element::started);
}
_refreshed.fire({});
}
void Provider::trackItemSession(not_null<const HistoryItem*> item) {
const auto session = &item->history()->session();
if (_trackedSessions.contains(session)) {
return;
}
auto &lifetime = _trackedSessions.emplace(session).first->second;
session->data().itemRemoved(
) | rpl::on_next([this](auto item) {
itemRemoved(item);
}, lifetime);
session->account().sessionChanges(
) | rpl::take(1) | rpl::on_next([=] {
_trackedSessions.remove(session);
}, lifetime);
}
rpl::producer<> Provider::refreshed() {
return _refreshed.events();
}
std::vector<ListSection> Provider::fillSections(
not_null<Overview::Layout::Delegate*> delegate) {
const auto search = searchMode();
if (!search) {
markLayoutsStale();
}
const auto guard = gsl::finally([&] { clearStaleLayouts(); });
if (_elements.empty() || (search && !_foundCount)) {
return {};
}
auto result = std::vector<ListSection>();
result.emplace_back(Type::File, sectionDelegate());
auto &section = result.back();
for (const auto &element : ranges::views::reverse(_elements)) {
if (search && !element.found) {
continue;
} else if (auto layout = getLayout(element, delegate)) {
section.addItem(layout);
}
}
section.finishSection();
return result;
}
void Provider::markLayoutsStale() {
for (auto &layout : _layouts) {
layout.second.stale = true;
}
}
void Provider::clearStaleLayouts() {
for (auto i = _layouts.begin(); i != _layouts.end();) {
if (i->second.stale) {
_layoutRemoved.fire(i->second.item.get());
i = _layouts.erase(i);
} else {
++i;
}
}
}
rpl::producer<not_null<BaseLayout*>> Provider::layoutRemoved() {
return _layoutRemoved.events();
}
BaseLayout *Provider::lookupLayout(const HistoryItem *item) {
return nullptr;
}
bool Provider::isMyItem(not_null<const HistoryItem*> item) {
return _downloading.contains(item) || _downloaded.contains(item);
}
bool Provider::isAfter(
not_null<const HistoryItem*> a,
not_null<const HistoryItem*> b) {
if (a != b) {
for (const auto &element : _elements) {
if (element.item == a) {
return false;
} else if (element.item == b) {
return true;
}
}
}
return false;
}
bool Provider::searchMode() const {
return !_queryWords.empty();
}
void Provider::fillSearchIndex(Element &element) {
auto strings = QStringList(QFileInfo(element.path).fileName());
if (const auto media = element.item->media()) {
if (const auto document = media->document()) {
strings.append(document->filename());
strings.append(Ui::Text::FormatDownloadsName(document).text);
}
}
element.words = TextUtilities::PrepareSearchWords(strings.join(' '));
element.letters.clear();
for (const auto &word : element.words) {
element.letters.emplace(word.front());
}
}
bool Provider::computeIsFound(const Element &element) const {
Expects(!_queryWords.empty());
const auto has = [&](const QString &queryWord) {
if (!element.letters.contains(queryWord.front())) {
return false;
}
for (const auto &word : element.words) {
if (word.startsWith(queryWord)) {
return true;
}
}
return false;
};
for (const auto &queryWord : _queryWords) {
if (!has(queryWord)) {
return false;
}
}
return true;
}
void Provider::itemRemoved(not_null<const HistoryItem*> item) {
remove(item);
}
BaseLayout *Provider::getLayout(
Element element,
not_null<Overview::Layout::Delegate*> delegate) {
auto it = _layouts.find(element.item);
if (it == _layouts.end()) {
if (auto layout = createLayout(element, delegate)) {
layout->initDimensions();
it = _layouts.emplace(element.item, std::move(layout)).first;
} else {
return nullptr;
}
}
it->second.stale = false;
return it->second.item.get();
}
std::unique_ptr<BaseLayout> Provider::createLayout(
Element element,
not_null<Overview::Layout::Delegate*> delegate) {
const auto getFile = [&]() -> DocumentData* {
if (auto media = element.item->media()) {
return media->document();
}
return nullptr;
};
using namespace Overview::Layout;
auto &songSt = st::overviewFileLayout;
if (const auto file = getFile()) {
return std::make_unique<Document>(
delegate,
element.item,
DocumentFields{
.document = file,
.dateOverride = Data::DateFromDownloadDate(element.started),
.forceFileLayout = true,
},
songSt);
}
return nullptr;
}
ListItemSelectionData Provider::computeSelectionData(
not_null<const HistoryItem*> item,
TextSelection selection) {
auto result = ListItemSelectionData(selection);
result.canDelete = true;
result.canForward = item->allowsForward()
&& (&item->history()->session() == &_controller->session());
return result;
}
void Provider::applyDragSelection(
ListSelectedMap &selected,
not_null<const HistoryItem*> fromItem,
bool skipFrom,
not_null<const HistoryItem*> tillItem,
bool skipTill) {
auto from = ranges::find(_elements, fromItem, &Element::item);
auto till = ranges::find(_elements, tillItem, &Element::item);
if (from == end(_elements) || till == end(_elements)) {
return;
}
if (skipFrom) {
++from;
}
if (!skipTill) {
++till;
}
if (from >= till) {
selected.clear();
return;
}
const auto search = !_queryWords.isEmpty();
const auto selectLimit = _storiesAddToAlbumId
? _controller->session().appConfig().storiesAlbumLimit()
: MaxSelectedItems;
auto chosen = base::flat_set<not_null<const HistoryItem*>>();
chosen.reserve(till - from);
for (auto i = from; i != till; ++i) {
if (search && !i->found) {
continue;
}
const auto item = i->item;
chosen.emplace(item);
ChangeItemSelection(
selected,
item,
computeSelectionData(item, FullSelection),
selectLimit);
}
if (selected.size() != chosen.size()) {
for (auto i = begin(selected); i != end(selected);) {
if (selected.contains(i->first)) {
++i;
} else {
i = selected.erase(i);
}
}
}
}
bool Provider::allowSaveFileAs(
not_null<const HistoryItem*> item,
not_null<DocumentData*> document) {
return false;
}
QString Provider::showInFolderPath(
not_null<const HistoryItem*> item,
not_null<DocumentData*> document) {
const auto i = ranges::find(_elements, item, &Element::item);
return (i != end(_elements)) ? i->path : QString();
}
int64 Provider::scrollTopStatePosition(not_null<HistoryItem*> item) {
const auto i = ranges::find(_elements, item, &Element::item);
return (i != end(_elements)) ? i->started : 0;
}
HistoryItem *Provider::scrollTopStateItem(ListScrollTopState state) {
if (!state.position) {
return _elements.empty() ? nullptr : _elements.back().item.get();
}
const auto i = ranges::lower_bound(
_elements,
state.position,
ranges::less(),
&Element::started);
return (i != end(_elements))
? i->item.get()
: _elements.empty()
? nullptr
: _elements.back().item.get();
}
void Provider::saveState(
not_null<Media::Memento*> memento,
ListScrollTopState scrollState) {
if (!_elements.empty() && scrollState.item) {
memento->setAroundId({ PeerId(), 1 });
memento->setScrollTopItem(scrollState.item->globalId());
memento->setScrollTopItemPosition(scrollState.position);
memento->setScrollTopShift(scrollState.shift);
}
}
void Provider::restoreState(
not_null<Media::Memento*> memento,
Fn<void(ListScrollTopState)> restoreScrollState) {
if (memento->aroundId() == FullMsgId(PeerId(), 1)) {
restoreScrollState({
.position = memento->scrollTopItemPosition(),
.item = MessageByGlobalId(memento->scrollTopItem()),
.shift = memento->scrollTopShift(),
});
refreshViewer();
}
}
} // namespace Info::Downloads

View File

@@ -0,0 +1,155 @@
/*
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 "info/media/info_media_common.h"
#include "base/weak_ptr.h"
namespace Data {
struct DownloadedId;
} // namespace Data
namespace Info {
class AbstractController;
} // namespace Info
namespace Info::Downloads {
class Provider final
: public Media::ListProvider
, private Media::ListSectionDelegate
, public base::has_weak_ptr {
public:
explicit Provider(not_null<AbstractController*> controller);
Media::Type type() override;
bool hasSelectRestriction() override;
rpl::producer<bool> hasSelectRestrictionChanges() override;
bool isPossiblyMyItem(not_null<const HistoryItem*> item) override;
std::optional<int> fullCount() override;
void restart() override;
void checkPreload(
QSize viewport,
not_null<Media::BaseLayout*> topLayout,
not_null<Media::BaseLayout*> bottomLayout,
bool preloadTop,
bool preloadBottom) override;
void refreshViewer() override;
rpl::producer<> refreshed() override;
void setSearchQuery(QString query) override;
std::vector<Media::ListSection> fillSections(
not_null<Overview::Layout::Delegate*> delegate) override;
rpl::producer<not_null<Media::BaseLayout*>> layoutRemoved() override;
Media::BaseLayout *lookupLayout(const HistoryItem *item) override;
bool isMyItem(not_null<const HistoryItem*> item) override;
bool isAfter(
not_null<const HistoryItem*> a,
not_null<const HistoryItem*> b) override;
Media::ListItemSelectionData computeSelectionData(
not_null<const HistoryItem*> item,
TextSelection selection) override;
void applyDragSelection(
Media::ListSelectedMap &selected,
not_null<const HistoryItem*> fromItem,
bool skipFrom,
not_null<const HistoryItem*> tillItem,
bool skipTill) override;
bool allowSaveFileAs(
not_null<const HistoryItem*> item,
not_null<DocumentData*> document) override;
QString showInFolderPath(
not_null<const HistoryItem*> item,
not_null<DocumentData*> document) override;
int64 scrollTopStatePosition(not_null<HistoryItem*> item) override;
HistoryItem *scrollTopStateItem(
Media::ListScrollTopState state) override;
void saveState(
not_null<Media::Memento*> memento,
Media::ListScrollTopState scrollState) override;
void restoreState(
not_null<Media::Memento*> memento,
Fn<void(Media::ListScrollTopState)> restoreScrollState) override;
private:
struct Element {
not_null<HistoryItem*> item;
int64 started = 0; // unixtime * 1000
QString path;
QStringList words;
base::flat_set<QChar> letters;
bool found = false;
};
bool sectionHasFloatingHeader() override;
QString sectionTitle(not_null<const Media::BaseLayout*> item) override;
bool sectionItemBelongsHere(
not_null<const Media::BaseLayout*> item,
not_null<const Media::BaseLayout*> previous) override;
[[nodiscard]] bool searchMode() const;
void fillSearchIndex(Element &element);
[[nodiscard]] bool computeIsFound(const Element &element) const;
void itemRemoved(not_null<const HistoryItem*> item);
void markLayoutsStale();
void clearStaleLayouts();
void refreshPostponed(bool added);
void addPostponed(not_null<const Data::DownloadedId*> entry);
void performRefresh();
void performAdd();
void addElementNow(Element &&element);
void remove(not_null<const HistoryItem*> item);
void trackItemSession(not_null<const HistoryItem*> item);
[[nodiscard]] Media::BaseLayout *getLayout(
Element element,
not_null<Overview::Layout::Delegate*> delegate);
[[nodiscard]] std::unique_ptr<Media::BaseLayout> createLayout(
Element element,
not_null<Overview::Layout::Delegate*> delegate);
const not_null<AbstractController*> _controller;
std::vector<Element> _elements;
std::optional<int> _fullCount;
base::flat_set<not_null<const HistoryItem*>> _downloading;
base::flat_set<not_null<const HistoryItem*>> _downloaded;
int _storiesAddToAlbumId = 0;
std::vector<Element> _addPostponed;
std::unordered_map<
not_null<const HistoryItem*>,
Media::CachedItem> _layouts;
rpl::event_stream<not_null<Media::BaseLayout*>> _layoutRemoved;
rpl::event_stream<> _refreshed;
QString _query;
QStringList _queryWords;
int _foundCount = 0;
base::flat_map<not_null<Main::Session*>, rpl::lifetime> _trackedSessions;
bool _postponedRefreshSort = false;
bool _postponedRefresh = false;
bool _started = false;
rpl::lifetime _lifetime;
};
} // namespace Info::Downloads

View File

@@ -0,0 +1,144 @@
/*
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 "info/downloads/info_downloads_widget.h"
#include "info/downloads/info_downloads_inner_widget.h"
#include "info/info_controller.h"
#include "info/info_memento.h"
#include "ui/boxes/confirm_box.h"
#include "ui/search_field_controller.h"
#include "ui/widgets/menu/menu_add_action_callback.h"
#include "ui/widgets/scroll_area.h"
#include "ui/ui_utility.h"
#include "data/data_download_manager.h"
#include "data/data_user.h"
#include "core/application.h"
#include "lang/lang_keys.h"
#include "styles/style_info.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
namespace Info::Downloads {
Memento::Memento(not_null<Controller*> controller)
: ContentMemento(Tag{})
, _media(controller) {
}
Memento::Memento(not_null<UserData*> self)
: ContentMemento(Tag{})
, _media(self, 0, Media::Type::File) {
}
Memento::~Memento() = default;
Section Memento::section() const {
return Section(Section::Type::Downloads);
}
object_ptr<ContentWidget> Memento::createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) {
auto result = object_ptr<Widget>(parent, controller);
result->setInternalState(geometry, this);
return result;
}
Widget::Widget(
QWidget *parent,
not_null<Controller*> controller)
: ContentWidget(parent, controller) {
_inner = setInnerWidget(object_ptr<InnerWidget>(
this,
controller));
_inner->setScrollHeightValue(scrollHeightValue());
_inner->scrollToRequests(
) | rpl::on_next([this](Ui::ScrollToRequest request) {
scrollTo(request);
}, _inner->lifetime());
}
bool Widget::showInternal(not_null<ContentMemento*> memento) {
if (auto downloadsMemento = dynamic_cast<Memento*>(memento.get())) {
restoreState(downloadsMemento);
return true;
}
return false;
}
void Widget::setInternalState(
const QRect &geometry,
not_null<Memento*> memento) {
setGeometry(geometry);
Ui::SendPendingMoveResizeEvents(this);
restoreState(memento);
}
std::shared_ptr<ContentMemento> Widget::doCreateMemento() {
auto result = std::make_shared<Memento>(controller());
saveState(result.get());
return result;
}
void Widget::saveState(not_null<Memento*> memento) {
memento->setScrollTop(scrollTopSave());
_inner->saveState(memento);
}
void Widget::restoreState(not_null<Memento*> memento) {
_inner->restoreState(memento);
scrollTopRestore(memento->scrollTop());
}
rpl::producer<SelectedItems> Widget::selectedListValue() const {
return _inner->selectedListValue();
}
void Widget::selectionAction(SelectionAction action) {
_inner->selectionAction(action);
}
void Widget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) {
const auto window = controller()->parentController();
const auto deleteAll = [=] {
auto &manager = Core::App().downloadManager();
const auto phrase = tr::lng_downloads_delete_sure_all(tr::now);
const auto added = manager.loadedHasNonCloudFile()
? QString()
: tr::lng_downloads_delete_in_cloud(tr::now);
const auto deleteSure = [=, &manager](Fn<void()> close) {
Ui::PostponeCall(this, close);
manager.deleteAll();
};
window->show(Ui::MakeConfirmBox({
.text = phrase + (added.isEmpty() ? QString() : "\n\n" + added),
.confirmed = deleteSure,
.confirmText = tr::lng_box_delete(tr::now),
.confirmStyle = &st::attentionBoxButton,
}));
};
addAction(
tr::lng_context_delete_all_files(tr::now),
deleteAll,
&st::menuIconDelete);
}
rpl::producer<QString> Widget::title() {
return tr::lng_downloads_section();
}
std::shared_ptr<Info::Memento> Make(not_null<UserData*> self) {
return std::make_shared<Info::Memento>(
std::vector<std::shared_ptr<ContentMemento>>(
1,
std::make_shared<Memento>(self)));
}
} // namespace Info::Downloads

View File

@@ -0,0 +1,76 @@
/*
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 "info/info_content_widget.h"
#include "info/media/info_media_widget.h"
namespace Ui {
class SearchFieldController;
} // namespace Ui
namespace Info::Downloads {
class InnerWidget;
class Memento final : public ContentMemento {
public:
Memento(not_null<Controller*> controller);
Memento(not_null<UserData*> self);
~Memento();
object_ptr<ContentWidget> createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) override;
Section section() const override;
[[nodiscard]] Media::Memento &media() {
return _media;
}
[[nodiscard]] const Media::Memento &media() const {
return _media;
}
private:
Media::Memento _media;
};
class Widget final : public ContentWidget {
public:
Widget(QWidget *parent, not_null<Controller*> controller);
bool showInternal(
not_null<ContentMemento*> memento) override;
void setInternalState(
const QRect &geometry,
not_null<Memento*> memento);
rpl::producer<SelectedItems> selectedListValue() const override;
void selectionAction(SelectionAction action) override;
void fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) override;
rpl::producer<QString> title() override;
private:
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
std::shared_ptr<ContentMemento> doCreateMemento() override;
InnerWidget *_inner = nullptr;
};
[[nodiscard]] std::shared_ptr<Info::Memento> Make(not_null<UserData*> self);
} // namespace Info::Downloads

View File

@@ -0,0 +1,151 @@
/*
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 "info/global_media/info_global_media_inner_widget.h"
#include "info/global_media/info_global_media_provider.h"
#include "info/global_media/info_global_media_widget.h"
#include "info/media/info_media_empty_widget.h"
#include "info/media/info_media_list_widget.h"
#include "info/info_controller.h"
#include "ui/widgets/labels.h"
#include "ui/search_field_controller.h"
#include "lang/lang_keys.h"
#include "styles/style_info.h"
namespace Info::GlobalMedia {
InnerWidget::InnerWidget(
QWidget *parent,
not_null<Controller*> controller)
: RpWidget(parent)
, _controller(controller)
, _empty(this) {
_empty->setType(type());
_empty->heightValue(
) | rpl::on_next(
[this] { refreshHeight(); },
_empty->lifetime());
_list = setupList();
}
object_ptr<Media::ListWidget> InnerWidget::setupList() {
auto result = object_ptr<Media::ListWidget>(this, _controller);
// Setup list widget connections
result->heightValue(
) | rpl::on_next([this] {
refreshHeight();
}, result->lifetime());
using namespace rpl::mappers;
result->scrollToRequests(
) | rpl::map([widget = result.data()](int to) {
return Ui::ScrollToRequest{
widget->y() + to,
-1
};
}) | rpl::start_to_stream(
_scrollToRequests,
result->lifetime());
_controller->searchQueryValue(
) | rpl::on_next([this](const QString &query) {
_empty->setSearchQuery(query);
}, result->lifetime());
return result;
}
Storage::SharedMediaType InnerWidget::type() const {
return _controller->section().mediaType();
}
void InnerWidget::visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) {
setChildVisibleTopBottom(_list, visibleTop, visibleBottom);
}
bool InnerWidget::showInternal(not_null<Memento*> memento) {
if (memento->section().type() == Section::Type::GlobalMedia
&& memento->section().mediaType() == type()) {
restoreState(memento);
return true;
}
return false;
}
void InnerWidget::saveState(not_null<Memento*> memento) {
_list->saveState(&memento->media());
}
void InnerWidget::restoreState(not_null<Memento*> memento) {
_list->restoreState(&memento->media());
}
rpl::producer<SelectedItems> InnerWidget::selectedListValue() const {
return _selectedLists.events_starting_with(
_list->selectedListValue()
) | rpl::flatten_latest();
}
void InnerWidget::selectionAction(SelectionAction action) {
_list->selectionAction(action);
}
InnerWidget::~InnerWidget() = default;
int InnerWidget::resizeGetHeight(int newWidth) {
_inResize = true;
auto guard = gsl::finally([this] { _inResize = false; });
_list->resizeToWidth(newWidth);
_empty->resizeToWidth(newWidth);
return recountHeight();
}
void InnerWidget::refreshHeight() {
if (_inResize) {
return;
}
resize(width(), recountHeight());
}
int InnerWidget::recountHeight() {
auto top = 0;
auto listHeight = 0;
if (_list) {
_list->moveToLeft(0, top);
listHeight = _list->heightNoMargins();
top += listHeight;
}
if (listHeight > 0) {
_empty->hide();
} else {
_empty->show();
_empty->moveToLeft(0, top);
top += _empty->heightNoMargins();
}
return top;
}
void InnerWidget::setScrollHeightValue(rpl::producer<int> value) {
using namespace rpl::mappers;
_empty->setFullHeight(rpl::combine(
std::move(value),
_listTops.events_starting_with(
_list->topValue()
) | rpl::flatten_latest(),
_1 - _2));
}
rpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {
return _scrollToRequests.events();
}
} // namespace Info::GlobalMedia

View File

@@ -0,0 +1,85 @@
/*
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/rp_widget.h"
#include "ui/widgets/scroll_area.h"
#include "base/unique_qptr.h"
namespace Ui {
class VerticalLayout;
class SearchFieldController;
} // namespace Ui
namespace Storage {
enum class SharedMediaType : signed char;
} // namespace Storage
namespace Info {
class Controller;
struct SelectedItems;
enum class SelectionAction;
} // namespace Info
namespace Info::Media {
class ListWidget;
class EmptyWidget;
} // namespace Info::Media
namespace Info::GlobalMedia {
class Memento;
class EmptyWidget;
class InnerWidget final : public Ui::RpWidget {
public:
InnerWidget(
QWidget *parent,
not_null<Controller*> controller);
bool showInternal(not_null<Memento*> memento);
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
void setScrollHeightValue(rpl::producer<int> value);
rpl::producer<Ui::ScrollToRequest> scrollToRequests() const;
rpl::producer<SelectedItems> selectedListValue() const;
void selectionAction(SelectionAction action);
~InnerWidget();
protected:
int resizeGetHeight(int newWidth) override;
void visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) override;
private:
int recountHeight();
void refreshHeight();
Storage::SharedMediaType type() const;
object_ptr<Media::ListWidget> setupList();
const not_null<Controller*> _controller;
object_ptr<Media::ListWidget> _list = { nullptr };
object_ptr<Media::EmptyWidget> _empty;
bool _inResize = false;
rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
rpl::event_stream<rpl::producer<SelectedItems>> _selectedLists;
rpl::event_stream<rpl::producer<int>> _listTops;
};
} // namespace Info::GlobalMedia

View File

@@ -0,0 +1,631 @@
/*
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 "info/global_media/info_global_media_provider.h"
#include "apiwrap.h"
#include "info/media/info_media_widget.h"
#include "info/media/info_media_list_section.h"
#include "info/info_controller.h"
#include "lang/lang_keys.h"
#include "ui/text/format_song_document_name.h"
#include "ui/ui_utility.h"
#include "data/data_document.h"
#include "data/data_media_types.h"
#include "data/data_session.h"
#include "main/main_session.h"
#include "main/main_account.h"
#include "history/history_item.h"
#include "history/history_item_helpers.h"
#include "history/history.h"
#include "core/application.h"
#include "storage/storage_shared_media.h"
#include "layout/layout_selection.h"
#include "styles/style_overview.h"
namespace Info::GlobalMedia {
namespace {
constexpr auto kPerPage = 50;
constexpr auto kPreloadedScreensCount = 4;
constexpr auto kPreloadedScreensCountFull
= kPreloadedScreensCount + 1 + kPreloadedScreensCount;
} // namespace
GlobalMediaSlice::GlobalMediaSlice(
Key key,
std::vector<Data::MessagePosition> items,
std::optional<int> fullCount,
int skippedAfter)
: _key(key)
, _items(std::move(items))
, _fullCount(fullCount)
, _skippedAfter(skippedAfter) {
}
std::optional<int> GlobalMediaSlice::fullCount() const {
return _fullCount;
}
std::optional<int> GlobalMediaSlice::skippedBefore() const {
return _fullCount
? int(*_fullCount - _skippedAfter - _items.size())
: std::optional<int>();
}
std::optional<int> GlobalMediaSlice::skippedAfter() const {
return _skippedAfter;
}
std::optional<int> GlobalMediaSlice::indexOf(Value position) const {
const auto it = ranges::find(_items, position);
return (it != end(_items))
? std::make_optional(int(it - begin(_items)))
: std::nullopt;
}
int GlobalMediaSlice::size() const {
return _items.size();
}
GlobalMediaSlice::Value GlobalMediaSlice::operator[](int index) const {
Expects(index >= 0 && index < size());
return _items[index];
}
std::optional<int> GlobalMediaSlice::distance(
const Key &a,
const Key &b) const {
const auto i = indexOf(a.aroundId);
const auto j = indexOf(b.aroundId);
return (i && j) ? std::make_optional(*j - *i) : std::nullopt;
}
std::optional<GlobalMediaSlice::Value> GlobalMediaSlice::nearest(
Value position) const {
if (_items.empty()) {
return std::nullopt;
}
const auto it = ranges::lower_bound(
_items,
position,
std::greater<>{});
if (it == end(_items)) {
return _items.back();
} else if (it == begin(_items)) {
return _items.front();
}
return *it;
}
Provider::Provider(not_null<AbstractController*> controller)
: _controller(controller)
, _type(_controller->section().mediaType())
, _slice(sliceKey(_aroundId)) {
_controller->session().data().itemRemoved(
) | rpl::on_next([this](auto item) {
itemRemoved(item);
}, _lifetime);
style::PaletteChanged(
) | rpl::on_next([=] {
for (auto &layout : _layouts) {
layout.second.item->invalidateCache();
}
}, _lifetime);
}
Provider::Type Provider::type() {
return _type;
}
bool Provider::hasSelectRestriction() {
return true;
}
rpl::producer<bool> Provider::hasSelectRestrictionChanges() {
return rpl::never<bool>();
}
bool Provider::sectionHasFloatingHeader() {
switch (_type) {
case Type::Photo:
case Type::GIF:
case Type::Video:
case Type::RoundFile:
case Type::RoundVoiceFile:
case Type::MusicFile:
return false;
case Type::File:
case Type::Link:
return true;
}
Unexpected("Type in HasFloatingHeader()");
}
QString Provider::sectionTitle(not_null<const BaseLayout*> item) {
return QString();
}
bool Provider::sectionItemBelongsHere(
not_null<const BaseLayout*> item,
not_null<const BaseLayout*> previous) {
return true;
}
bool Provider::isPossiblyMyItem(not_null<const HistoryItem*> item) {
return item->media() != nullptr;
}
std::optional<int> Provider::fullCount() {
return _slice.fullCount();
}
void Provider::restart() {
_layouts.clear();
_aroundId = Data::MaxMessagePosition;
_idsLimit = kMinimalIdsLimit;
_slice = GlobalMediaSlice(sliceKey(_aroundId));
refreshViewer();
}
void Provider::checkPreload(
QSize viewport,
not_null<BaseLayout*> topLayout,
not_null<BaseLayout*> bottomLayout,
bool preloadTop,
bool preloadBottom) {
const auto visibleWidth = viewport.width();
const auto visibleHeight = viewport.height();
const auto preloadedHeight = kPreloadedScreensCountFull * visibleHeight;
const auto minItemHeight = Media::MinItemHeight(_type, visibleWidth);
const auto preloadedCount = preloadedHeight / minItemHeight;
const auto preloadIdsLimitMin = (preloadedCount / 2) + 1;
const auto preloadIdsLimit = preloadIdsLimitMin
+ (visibleHeight / minItemHeight);
const auto after = _slice.skippedAfter();
const auto topLoaded = after && (*after == 0);
const auto before = _slice.skippedBefore();
const auto bottomLoaded = before && (*before == 0);
const auto minScreenDelta = kPreloadedScreensCount
- Media::kPreloadIfLessThanScreens;
const auto minUniversalIdDelta = (minScreenDelta * visibleHeight)
/ minItemHeight;
const auto preloadAroundItem = [&](not_null<BaseLayout*> layout) {
auto preloadRequired = false;
auto aroundId = layout->getItem()->position();
if (!preloadRequired) {
preloadRequired = (_idsLimit < preloadIdsLimitMin);
}
if (!preloadRequired) {
auto delta = _slice.distance(
sliceKey(_aroundId),
sliceKey(aroundId));
Assert(delta != std::nullopt);
preloadRequired = (qAbs(*delta) >= minUniversalIdDelta);
}
if (preloadRequired) {
_idsLimit = preloadIdsLimit;
_aroundId = aroundId;
refreshViewer();
}
};
if (preloadTop && !topLoaded) {
preloadAroundItem(topLayout);
} else if (preloadBottom && !bottomLoaded) {
preloadAroundItem(bottomLayout);
}
}
rpl::producer<GlobalMediaSlice> Provider::source(
Type type,
Data::MessagePosition aroundId,
QString query,
int limitBefore,
int limitAfter) {
Expects(_type == type);
_totalListQuery = query;
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
const auto session = &_controller->session();
struct State : base::has_weak_ptr {
State(not_null<Main::Session*> session) : session(session) {
}
~State() {
session->api().request(requestId).cancel();
}
const not_null<Main::Session*> session;
Fn<void()> pushAndLoadMore;
mtpRequestId requestId = 0;
};
const auto state = lifetime.make_state<State>(session);
const auto guard = base::make_weak(state);
state->pushAndLoadMore = [=] {
auto result = fillRequest(aroundId, limitBefore, limitAfter);
// May destroy 'state' by calling source() with different args.
consumer.put_next(std::move(result.slice));
if (guard && !currentList()->loaded && result.notEnough) {
state->requestId = requestMore(state->pushAndLoadMore);
}
};
state->pushAndLoadMore();
return lifetime;
};
}
mtpRequestId Provider::requestMore(Fn<void()> loaded) {
const auto done = [=](const Api::GlobalMediaResult &result) {
const auto list = currentList();
if (result.messageIds.empty()) {
list->loaded = true;
list->fullCount = list->list.size();
} else {
list->list.reserve(list->list.size() + result.messageIds.size());
list->fullCount = result.fullCount;
for (const auto &position : result.messageIds) {
_seenIds.emplace(position.fullId);
list->offsetPosition = position;
list->list.push_back(position);
}
}
if (!result.offsetRate) {
list->loaded = true;
} else {
list->offsetRate = result.offsetRate;
}
loaded();
};
const auto list = currentList();
return _controller->session().api().requestGlobalMedia(
_type,
_totalListQuery,
list->offsetRate,
list->offsetPosition,
done);
}
Provider::FillResult Provider::fillRequest(
Data::MessagePosition aroundId,
int limitBefore,
int limitAfter) {
const auto list = currentList();
const auto i = ranges::lower_bound(
list->list,
aroundId,
std::greater<>());
const auto hasAfter = int(i - begin(list->list));
const auto hasBefore = int(end(list->list) - i);
const auto takeAfter = std::min(limitAfter, hasAfter);
const auto takeBefore = std::min(limitBefore, hasBefore);
auto messages = std::vector<Data::MessagePosition>{
i - takeAfter,
i + takeBefore,
};
return FillResult{
.slice = GlobalMediaSlice(
GlobalMediaKey{ aroundId },
std::move(messages),
((!list->list.empty() || list->loaded)
? list->fullCount
: std::optional<int>()),
hasAfter - takeAfter),
.notEnough = (takeBefore < limitBefore),
};
}
void Provider::refreshViewer() {
_viewerLifetime.destroy();
_controller->searchQueryValue(
) | rpl::map([=](QString query) {
return source(
_type,
sliceKey(_aroundId).aroundId,
query,
_idsLimit,
_idsLimit);
}) | rpl::flatten_latest(
) | rpl::on_next([=](GlobalMediaSlice &&slice) {
if (!slice.fullCount()) {
// Don't display anything while full count is unknown.
return;
}
_slice = std::move(slice);
if (auto nearest = _slice.nearest(_aroundId)) {
_aroundId = *nearest;
}
_refreshed.fire({});
}, _viewerLifetime);
}
rpl::producer<> Provider::refreshed() {
return _refreshed.events();
}
std::vector<Media::ListSection> Provider::fillSections(
not_null<Overview::Layout::Delegate*> delegate) {
markLayoutsStale();
const auto guard = gsl::finally([&] { clearStaleLayouts(); });
auto result = std::vector<Media::ListSection>();
result.emplace_back(_type, sectionDelegate());
auto &section = result.back();
for (auto i = 0, count = int(_slice.size()); i != count; ++i) {
auto position = _slice[i];
if (auto layout = getLayout(position.fullId, delegate)) {
section.addItem(layout);
}
}
if (section.empty()) {
result.pop_back();
}
return result;
}
void Provider::markLayoutsStale() {
for (auto &layout : _layouts) {
layout.second.stale = true;
}
}
void Provider::clearStaleLayouts() {
for (auto i = _layouts.begin(); i != _layouts.end();) {
if (i->second.stale) {
_layoutRemoved.fire(i->second.item.get());
i = _layouts.erase(i);
} else {
++i;
}
}
}
Provider::List *Provider::currentList() {
return &_totalLists[_totalListQuery];
}
rpl::producer<not_null<Media::BaseLayout*>> Provider::layoutRemoved() {
return _layoutRemoved.events();
}
Media::BaseLayout *Provider::lookupLayout(
const HistoryItem *item) {
const auto i = _layouts.find(item ? item->fullId() : FullMsgId());
return (i != _layouts.end()) ? i->second.item.get() : nullptr;
}
bool Provider::isMyItem(not_null<const HistoryItem*> item) {
return _seenIds.contains(item->fullId());
}
bool Provider::isAfter(
not_null<const HistoryItem*> a,
not_null<const HistoryItem*> b) {
return (a->fullId() < b->fullId());
}
void Provider::setSearchQuery(QString query) {
Unexpected("Media::Provider::setSearchQuery.");
}
GlobalMediaKey Provider::sliceKey(Data::MessagePosition aroundId) const {
return GlobalMediaKey{ aroundId };
}
void Provider::itemRemoved(not_null<const HistoryItem*> item) {
const auto id = item->fullId();
if (const auto i = _layouts.find(id); i != end(_layouts)) {
_layoutRemoved.fire(i->second.item.get());
_layouts.erase(i);
}
}
Media::BaseLayout *Provider::getLayout(
FullMsgId itemId,
not_null<Overview::Layout::Delegate*> delegate) {
auto it = _layouts.find(itemId);
if (it == _layouts.end()) {
if (auto layout = createLayout(itemId, delegate, _type)) {
layout->initDimensions();
it = _layouts.emplace(
itemId,
std::move(layout)).first;
} else {
return nullptr;
}
}
it->second.stale = false;
return it->second.item.get();
}
std::unique_ptr<Media::BaseLayout> Provider::createLayout(
FullMsgId itemId,
not_null<Overview::Layout::Delegate*> delegate,
Type type) {
const auto item = _controller->session().data().message(itemId);
if (!item) {
return nullptr;
}
const auto getPhoto = [&]() -> PhotoData* {
if (const auto media = item->media()) {
return media->photo();
}
return nullptr;
};
const auto getFile = [&]() -> DocumentData* {
if (const auto media = item->media()) {
return media->document();
}
return nullptr;
};
const auto &songSt = st::overviewFileLayout;
using namespace Overview::Layout;
const auto options = [&] {
const auto media = item->media();
return MediaOptions{ .spoiler = media && media->hasSpoiler() };
};
switch (type) {
case Type::Photo:
if (const auto photo = getPhoto()) {
return std::make_unique<Photo>(
delegate,
item,
photo,
options());
}
return nullptr;
case Type::GIF:
if (const auto file = getFile()) {
return std::make_unique<Gif>(delegate, item, file);
}
return nullptr;
case Type::Video:
if (const auto file = getFile()) {
return std::make_unique<Video>(delegate, item, file, options());
}
return nullptr;
case Type::File:
if (const auto file = getFile()) {
return std::make_unique<Document>(
delegate,
item,
DocumentFields{ .document = file },
songSt);
}
return nullptr;
case Type::MusicFile:
if (const auto file = getFile()) {
return std::make_unique<Document>(
delegate,
item,
DocumentFields{ .document = file },
songSt);
}
return nullptr;
case Type::RoundVoiceFile:
if (const auto file = getFile()) {
return std::make_unique<Voice>(delegate, item, file, songSt);
}
return nullptr;
case Type::Link:
return std::make_unique<Link>(delegate, item, item->media());
case Type::RoundFile:
return nullptr;
}
Unexpected("Type in ListWidget::createLayout()");
}
Media::ListItemSelectionData Provider::computeSelectionData(
not_null<const HistoryItem*> item,
TextSelection selection) {
auto result = Media::ListItemSelectionData(selection);
result.canDelete = item->canDelete();
result.canForward = item->allowsForward();
return result;
}
bool Provider::allowSaveFileAs(
not_null<const HistoryItem*> item,
not_null<DocumentData*> document) {
return item->allowsForward();
}
QString Provider::showInFolderPath(
not_null<const HistoryItem*> item,
not_null<DocumentData*> document) {
return document->filepath(true);
}
void Provider::applyDragSelection(
Media::ListSelectedMap &selected,
not_null<const HistoryItem*> fromItem,
bool skipFrom,
not_null<const HistoryItem*> tillItem,
bool skipTill) {
#if 0 // not used for now
const auto fromId = GetUniversalId(fromItem) - (skipFrom ? 1 : 0);
const auto tillId = GetUniversalId(tillItem) - (skipTill ? 0 : 1);
for (auto i = selected.begin(); i != selected.end();) {
const auto itemId = GetUniversalId(i->first);
if (itemId > fromId || itemId <= tillId) {
i = selected.erase(i);
} else {
++i;
}
}
for (auto &layoutItem : _layouts) {
auto &&universalId = layoutItem.first;
if (universalId <= fromId && universalId > tillId) {
const auto item = layoutItem.second.item->getItem();
ChangeItemSelection(
selected,
item,
computeSelectionData(item, FullSelection));
}
}
#endif // todo global media
}
int64 Provider::scrollTopStatePosition(not_null<HistoryItem*> item) {
return item->position().date;
}
HistoryItem *Provider::scrollTopStateItem(Media::ListScrollTopState state) {
const auto maybe = Data::MessagePosition{
.date = TimeId(state.position),
};
if (state.item && _slice.indexOf(state.item->position())) {
return state.item;
} else if (const auto position = _slice.nearest(maybe)) {
const auto id = position->fullId;
if (const auto item = _controller->session().data().message(id)) {
return item;
}
}
return state.item;
}
void Provider::saveState(
not_null<Media::Memento*> memento,
Media::ListScrollTopState scrollState) {
if (_aroundId != Data::MaxMessagePosition && scrollState.item) {
memento->setAroundId(_aroundId.fullId);
memento->setIdsLimit(_idsLimit);
memento->setScrollTopItem(scrollState.item->globalId());
memento->setScrollTopItemPosition(scrollState.position);
memento->setScrollTopShift(scrollState.shift);
}
}
void Provider::restoreState(
not_null<Media::Memento*> memento,
Fn<void(Media::ListScrollTopState)> restoreScrollState) {
if (const auto limit = memento->idsLimit()) {
_idsLimit = limit;
_aroundId = { memento->aroundId() };
restoreScrollState({
.position = memento->scrollTopItemPosition(),
.item = MessageByGlobalId(memento->scrollTopItem()),
.shift = memento->scrollTopShift(),
});
refreshViewer();
}
}
} // namespace Info::GlobalMedia

View File

@@ -0,0 +1,191 @@
/*
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 "data/data_messages.h"
#include "info/media/info_media_common.h"
#include "base/weak_ptr.h"
namespace Info {
class AbstractController;
} // namespace Info
namespace Info::GlobalMedia {
struct GlobalMediaKey {
Data::MessagePosition aroundId;
friend inline constexpr bool operator==(
const GlobalMediaKey &,
const GlobalMediaKey &) = default;
};
class GlobalMediaSlice final {
public:
using Key = GlobalMediaKey;
using Value = Data::MessagePosition;
explicit GlobalMediaSlice(
Key key,
std::vector<Data::MessagePosition> items = {},
std::optional<int> fullCount = std::nullopt,
int skippedAfter = 0);
[[nodiscard]] std::optional<int> fullCount() const;
[[nodiscard]] std::optional<int> skippedBefore() const;
[[nodiscard]] std::optional<int> skippedAfter() const;
[[nodiscard]] std::optional<int> indexOf(Value fullId) const;
[[nodiscard]] int size() const;
[[nodiscard]] Value operator[](int index) const;
[[nodiscard]] std::optional<int> distance(
const Key &a,
const Key &b) const;
[[nodiscard]] std::optional<Value> nearest(Value id) const;
private:
GlobalMediaKey _key;
std::vector<Data::MessagePosition> _items;
std::optional<int> _fullCount;
int _skippedAfter = 0;
};
class Provider final
: public Media::ListProvider
, private Media::ListSectionDelegate {
public:
using Type = Media::Type;
using BaseLayout = Media::BaseLayout;
explicit Provider(not_null<AbstractController*> controller);
Type type() override;
bool hasSelectRestriction() override;
rpl::producer<bool> hasSelectRestrictionChanges() override;
bool isPossiblyMyItem(not_null<const HistoryItem*> item) override;
std::optional<int> fullCount() override;
void restart() override;
void checkPreload(
QSize viewport,
not_null<BaseLayout*> topLayout,
not_null<BaseLayout*> bottomLayout,
bool preloadTop,
bool preloadBottom) override;
void refreshViewer() override;
rpl::producer<> refreshed() override;
std::vector<Media::ListSection> fillSections(
not_null<Overview::Layout::Delegate*> delegate) override;
rpl::producer<not_null<BaseLayout*>> layoutRemoved() override;
BaseLayout *lookupLayout(const HistoryItem *item) override;
bool isMyItem(not_null<const HistoryItem*> item) override;
bool isAfter(
not_null<const HistoryItem*> a,
not_null<const HistoryItem*> b) override;
void setSearchQuery(QString query) override;
Media::ListItemSelectionData computeSelectionData(
not_null<const HistoryItem*> item,
TextSelection selection) override;
void applyDragSelection(
Media::ListSelectedMap &selected,
not_null<const HistoryItem*> fromItem,
bool skipFrom,
not_null<const HistoryItem*> tillItem,
bool skipTill) override;
bool allowSaveFileAs(
not_null<const HistoryItem*> item,
not_null<DocumentData*> document) override;
QString showInFolderPath(
not_null<const HistoryItem*> item,
not_null<DocumentData*> document) override;
int64 scrollTopStatePosition(not_null<HistoryItem*> item) override;
HistoryItem *scrollTopStateItem(
Media::ListScrollTopState state) override;
void saveState(
not_null<Media::Memento*> memento,
Media::ListScrollTopState scrollState) override;
void restoreState(
not_null<Media::Memento*> memento,
Fn<void(Media::ListScrollTopState)> restoreScrollState) override;
private:
static constexpr auto kMinimalIdsLimit = 16;
struct FillResult {
GlobalMediaSlice slice;
bool notEnough = false;
};
struct List {
std::vector<Data::MessagePosition> list;
Data::MessagePosition offsetPosition;
int32 offsetRate = 0;
int fullCount = 0;
bool loaded = false;
};
bool sectionHasFloatingHeader() override;
QString sectionTitle(not_null<const BaseLayout*> item) override;
bool sectionItemBelongsHere(
not_null<const BaseLayout*> item,
not_null<const BaseLayout*> previous) override;
[[nodiscard]] rpl::producer<GlobalMediaSlice> source(
Type type,
Data::MessagePosition aroundId,
QString query,
int limitBefore,
int limitAfter);
[[nodiscard]] BaseLayout *getLayout(
FullMsgId itemId,
not_null<Overview::Layout::Delegate*> delegate);
[[nodiscard]] std::unique_ptr<BaseLayout> createLayout(
FullMsgId itemId,
not_null<Overview::Layout::Delegate*> delegate,
Type type);
[[nodiscard]] GlobalMediaKey sliceKey(
Data::MessagePosition aroundId) const;
void itemRemoved(not_null<const HistoryItem*> item);
void markLayoutsStale();
void clearStaleLayouts();
[[nodiscard]] List *currentList();
[[nodiscard]] FillResult fillRequest(
Data::MessagePosition aroundId,
int limitBefore,
int limitAfter);
mtpRequestId requestMore(Fn<void()> loaded);
const not_null<AbstractController*> _controller;
const Type _type = {};
Data::MessagePosition _aroundId = Data::MaxMessagePosition;
int _idsLimit = kMinimalIdsLimit;
GlobalMediaSlice _slice;
base::flat_set<FullMsgId> _seenIds;
std::unordered_map<FullMsgId, Media::CachedItem> _layouts;
rpl::event_stream<not_null<BaseLayout*>> _layoutRemoved;
rpl::event_stream<> _refreshed;
QString _totalListQuery;
base::flat_map<QString, List> _totalLists;
rpl::lifetime _lifetime;
rpl::lifetime _viewerLifetime;
};
} // namespace Info::GlobalMedia

View File

@@ -0,0 +1,144 @@
/*
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 "info/global_media/info_global_media_widget.h"
#include "info/global_media/info_global_media_inner_widget.h"
#include "info/info_controller.h"
#include "info/info_memento.h"
#include "main/main_session.h"
#include "ui/boxes/confirm_box.h"
#include "ui/search_field_controller.h"
#include "ui/widgets/menu/menu_add_action_callback.h"
#include "ui/widgets/scroll_area.h"
#include "ui/ui_utility.h"
#include "data/data_download_manager.h"
#include "data/data_user.h"
#include "core/application.h"
#include "lang/lang_keys.h"
#include "styles/style_info.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
namespace Info::GlobalMedia {
Memento::Memento(not_null<Controller*> controller)
: ContentMemento(Tag{ controller->session().user() })
, _media(controller) {
}
Memento::Memento(not_null<UserData*> self, Storage::SharedMediaType type)
: ContentMemento(Tag{ self })
, _media(self, 0, type) {
}
Memento::~Memento() = default;
Section Memento::section() const {
return Section(_media.type(), Section::Type::GlobalMedia);
}
object_ptr<ContentWidget> Memento::createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) {
auto result = object_ptr<Widget>(parent, controller);
result->setInternalState(geometry, this);
return result;
}
Widget::Widget(QWidget *parent, not_null<Controller*> controller)
: ContentWidget(parent, controller) {
_inner = setInnerWidget(object_ptr<InnerWidget>(
this,
controller));
_inner->setScrollHeightValue(scrollHeightValue());
_inner->scrollToRequests(
) | rpl::on_next([this](Ui::ScrollToRequest request) {
scrollTo(request);
}, _inner->lifetime());
}
bool Widget::showInternal(not_null<ContentMemento*> memento) {
if (auto globalMediaMemento = dynamic_cast<Memento*>(memento.get())) {
restoreState(globalMediaMemento);
return true;
}
return false;
}
void Widget::setInternalState(
const QRect &geometry,
not_null<Memento*> memento) {
setGeometry(geometry);
Ui::SendPendingMoveResizeEvents(this);
restoreState(memento);
}
std::shared_ptr<ContentMemento> Widget::doCreateMemento() {
auto result = std::make_shared<Memento>(controller());
saveState(result.get());
return result;
}
void Widget::saveState(not_null<Memento*> memento) {
memento->setScrollTop(scrollTopSave());
_inner->saveState(memento);
}
void Widget::restoreState(not_null<Memento*> memento) {
_inner->restoreState(memento);
scrollTopRestore(memento->scrollTop());
}
rpl::producer<SelectedItems> Widget::selectedListValue() const {
return _inner->selectedListValue();
}
void Widget::selectionAction(SelectionAction action) {
_inner->selectionAction(action);
}
void Widget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) {
const auto window = controller()->parentController();
const auto deleteAll = [=] {
auto &manager = Core::App().downloadManager();
const auto phrase = tr::lng_downloads_delete_sure_all(tr::now);
const auto added = manager.loadedHasNonCloudFile()
? QString()
: tr::lng_downloads_delete_in_cloud(tr::now);
const auto deleteSure = [=, &manager](Fn<void()> close) {
Ui::PostponeCall(this, close);
manager.deleteAll();
};
window->show(Ui::MakeConfirmBox({
.text = phrase + (added.isEmpty() ? QString() : "\n\n" + added),
.confirmed = deleteSure,
.confirmText = tr::lng_box_delete(tr::now),
.confirmStyle = &st::attentionBoxButton,
}));
};
addAction(
tr::lng_context_delete_all_files(tr::now),
deleteAll,
&st::menuIconDelete);
}
rpl::producer<QString> Widget::title() {
return tr::lng_profile_shared_media();
}
std::shared_ptr<Info::Memento> Make(
not_null<UserData*> self,
Storage::SharedMediaType type) {
return std::make_shared<Info::Memento>(
std::vector<std::shared_ptr<ContentMemento>>(
1,
std::make_shared<Memento>(self, type)));
}
} // namespace Info::GlobalMedia

View File

@@ -0,0 +1,78 @@
/*
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 "info/info_content_widget.h"
#include "info/media/info_media_widget.h"
namespace Storage {
enum class SharedMediaType : signed char;
} // namespace Storage
namespace Info::GlobalMedia {
class InnerWidget;
class Memento final : public ContentMemento {
public:
Memento(not_null<Controller*> controller);
Memento(not_null<UserData*> self, Storage::SharedMediaType type);
~Memento();
object_ptr<ContentWidget> createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) override;
Section section() const override;
[[nodiscard]] Media::Memento &media() {
return _media;
}
[[nodiscard]] const Media::Memento &media() const {
return _media;
}
private:
Media::Memento _media;
};
class Widget final : public ContentWidget {
public:
Widget(QWidget *parent, not_null<Controller*> controller);
bool showInternal(
not_null<ContentMemento*> memento) override;
void setInternalState(
const QRect &geometry,
not_null<Memento*> memento);
rpl::producer<SelectedItems> selectedListValue() const override;
void selectionAction(SelectionAction action) override;
void fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) override;
rpl::producer<QString> title() override;
private:
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
std::shared_ptr<ContentMemento> doCreateMemento() override;
InnerWidget *_inner = nullptr;
};
[[nodiscard]] std::shared_ptr<Info::Memento> Make(
not_null<UserData*> self,
Storage::SharedMediaType type);
} // namespace Info::GlobalMedia

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,635 @@
/*
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 "info/info_content_widget.h"
#include "api/api_who_reacted.h"
#include "boxes/peer_list_box.h"
#include "data/data_chat.h"
#include "data/data_channel.h"
#include "data/data_session.h"
#include "data/data_forum_topic.h"
#include "data/data_forum.h"
#include "info/profile/info_profile_widget.h"
#include "info/media/info_media_widget.h"
#include "info/common_groups/info_common_groups_widget.h"
#include "info/peer_gifts/info_peer_gifts_common.h"
#include "info/saved/info_saved_music_common.h"
#include "info/stories/info_stories_common.h"
#include "info/info_layer_widget.h"
#include "info/info_section_widget.h"
#include "info/info_controller.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/controls/swipe_handler.h"
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/search_field_controller.h"
#include "ui/ui_utility.h"
#include "window/window_peer_menu.h"
#include "window/window_session_controller.h"
#include "styles/style_info.h"
#include "styles/style_profile.h"
#include "styles/style_layers.h"
#include <QtCore/QCoreApplication>
namespace Info {
namespace {
class FlexibleFiller final : public Ui::RpWidget {
public:
using RpWidget::RpWidget;
void setTargetWidget(base::unique_qptr<RpWidget> widget);
private:
void visibleTopBottomUpdated(int visibleTop, int visibleBottom) override;
base::unique_qptr<RpWidget> _target;
};
void FlexibleFiller::setTargetWidget(base::unique_qptr<RpWidget> widget) {
Expects(!_target);
_target = std::move(widget);
}
void FlexibleFiller::visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) {
if (const auto raw = _target.get()) {
raw->setVisibleTopBottom(visibleTop, visibleBottom);
}
}
} // namespace
ContentWidget::ContentWidget(
QWidget *parent,
not_null<Controller*> controller)
: RpWidget(parent)
, _controller(controller)
, _scroll(
this,
(_controller->wrap() == Wrap::Search
? st::infoSharedMediaScroll
: st::defaultScrollArea)) {
using namespace rpl::mappers;
setAttribute(Qt::WA_OpaquePaintEvent);
_controller->wrapValue(
) | rpl::on_next([this](Wrap value) {
if (value != Wrap::Layer) {
applyAdditionalScroll(0);
}
_bg = (value == Wrap::Layer)
? st::boxBg
: st::profileBg;
update();
}, lifetime());
if (_controller->section().type() != Section::Type::Profile) {
rpl::combine(
_controller->wrapValue(),
_controller->searchEnabledByContent(),
(_1 == Wrap::Layer) && _2
) | rpl::distinct_until_changed(
) | rpl::on_next([this](bool shown) {
refreshSearchField(shown);
}, lifetime());
}
rpl::merge(
_scrollTopSkip.changes(),
_scrollBottomSkip.changes()
) | rpl::on_next([this] {
updateControlsGeometry();
}, lifetime());
}
void ContentWidget::resizeEvent(QResizeEvent *e) {
updateControlsGeometry();
}
void ContentWidget::updateControlsGeometry() {
if (!_innerWrap) {
return;
}
_innerWrap->resizeToWidth(width());
auto newScrollTop = _scroll->scrollTop() + _topDelta;
auto scrollGeometry = rect().marginsRemoved(
{ 0, _scrollTopSkip.current(), 0, _scrollBottomSkip.current() });
if (_scroll->geometry() != scrollGeometry) {
_scroll->setGeometry(scrollGeometry);
}
if (!_scroll->isHidden()) {
if (_topDelta) {
_scroll->scrollToY(newScrollTop);
}
auto scrollTop = _scroll->scrollTop();
_innerWrap->setVisibleTopBottom(
scrollTop,
scrollTop + _scroll->height());
}
}
std::shared_ptr<ContentMemento> ContentWidget::createMemento() {
auto result = doCreateMemento();
_controller->saveSearchState(result.get());
return result;
}
void ContentWidget::setIsStackBottom(bool isStackBottom) {
_isStackBottom = isStackBottom;
}
bool ContentWidget::isStackBottom() const {
return _isStackBottom;
}
void ContentWidget::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
if (_paintPadding.isNull()) {
p.fillRect(e->rect(), _bg);
} else {
const auto &r = e->rect();
const auto padding = QMargins(
0,
std::min(0, (r.top() - _paintPadding.top())),
0,
std::min(0, (r.bottom() - _paintPadding.bottom())));
p.fillRect(r + padding, _bg);
}
}
void ContentWidget::setGeometryWithTopMoved(
const QRect &newGeometry,
int topDelta) {
_topDelta = topDelta;
auto willBeResized = (size() != newGeometry.size());
if (geometry() != newGeometry) {
setGeometry(newGeometry);
}
if (!willBeResized) {
QResizeEvent fake(size(), size());
QCoreApplication::sendEvent(this, &fake);
}
_topDelta = 0;
}
Ui::RpWidget *ContentWidget::doSetInnerWidget(
object_ptr<RpWidget> inner) {
using namespace rpl::mappers;
_innerWrap = _scroll->setOwnedWidget(
object_ptr<Ui::PaddingWrap<Ui::RpWidget>>(
this,
std::move(inner),
_innerWrap ? _innerWrap->padding() : style::margins()));
_innerWrap->move(0, 0);
setupSwipeHandler(_innerWrap);
// MSVC BUG + REGRESSION rpl::mappers::tuple :(
rpl::combine(
_scroll->scrollTopValue(),
_scroll->heightValue(),
_innerWrap->entity()->desiredHeightValue()
) | rpl::on_next([this](
int top,
int height,
int desired) {
const auto bottom = top + height;
_innerDesiredHeight = desired;
_innerWrap->setVisibleTopBottom(top, bottom);
_scrollTillBottomChanges.fire_copy(std::max(desired - bottom, 0));
}, _innerWrap->lifetime());
rpl::combine(
_scroll->heightValue(),
_innerWrap->entity()->heightValue(),
_controller->wrapValue()
) | rpl::on_next([=](
int scrollHeight,
int innerHeight,
Wrap wrap) {
const auto added = (wrap == Wrap::Layer)
? 0
: std::max(scrollHeight - innerHeight, 0);
if (_addedHeight != added) {
_addedHeight = added;
updateInnerPadding();
}
}, _innerWrap->lifetime());
updateInnerPadding();
return _innerWrap->entity();
}
Ui::RpWidget *ContentWidget::doSetupFlexibleInnerWidget(
object_ptr<Ui::RpWidget> inner,
FlexibleScrollData &flexibleScroll,
Fn<void(Ui::RpWidget*)> customSetup) {
const auto filler = setInnerWidget(object_ptr<FlexibleFiller>(this));
filler->resize(1, 1);
flexibleScroll.contentHeightValue.events(
) | rpl::on_next([=](int h) {
filler->resize(filler->width(), h);
}, filler->lifetime());
filler->widthValue(
) | rpl::start_to_stream(
flexibleScroll.fillerWidthValue,
filler->lifetime());
if (customSetup) {
customSetup(filler);
}
// ScrollArea -> PaddingWrap -> RpWidget.
const auto result = inner.release();
result->setParent(filler->parentWidget()->parentWidget());
result->raise();
filler->setTargetWidget(base::unique_qptr<Ui::RpWidget>(result));
return result;
}
int ContentWidget::scrollTillBottom(int forHeight) const {
const auto scrollHeight = forHeight
- _scrollTopSkip.current()
- _scrollBottomSkip.current();
const auto scrollBottom = _scroll->scrollTop() + scrollHeight;
const auto desired = _innerDesiredHeight;
return std::max(desired - scrollBottom, 0);
}
rpl::producer<int> ContentWidget::scrollTillBottomChanges() const {
return _scrollTillBottomChanges.events();
}
void ContentWidget::setScrollTopSkip(int scrollTopSkip) {
_scrollTopSkip = scrollTopSkip;
}
void ContentWidget::setScrollBottomSkip(int scrollBottomSkip) {
_scrollBottomSkip = scrollBottomSkip;
}
rpl::producer<int> ContentWidget::scrollHeightValue() const {
return _scroll->heightValue();
}
void ContentWidget::applyAdditionalScroll(int additionalScroll) {
if (_additionalScroll != additionalScroll) {
_additionalScroll = additionalScroll;
if (_innerWrap) {
updateInnerPadding();
}
}
}
void ContentWidget::updateInnerPadding() {
const auto addedToBottom = std::max(_additionalScroll, _addedHeight);
_innerWrap->setPadding({ 0, 0, 0, addedToBottom });
}
void ContentWidget::applyMaxVisibleHeight(int maxVisibleHeight) {
if (_maxVisibleHeight != maxVisibleHeight) {
_maxVisibleHeight = maxVisibleHeight;
update();
}
}
rpl::producer<int> ContentWidget::desiredHeightValue() const {
using namespace rpl::mappers;
return rpl::combine(
_innerWrap->entity()->desiredHeightValue(),
_scrollTopSkip.value(),
_scrollBottomSkip.value()
//) | rpl::map(_1 + _2 + _3);
) | rpl::map([=](int desired, int, int) {
return desired
+ _scrollTopSkip.current()
+ _scrollBottomSkip.current();
});
}
rpl::producer<bool> ContentWidget::desiredShadowVisibility() const {
using namespace rpl::mappers;
return rpl::combine(
_scroll->scrollTopValue(),
_scrollTopSkip.value()
) | rpl::map((_1 > 0) || (_2 > 0));
}
bool ContentWidget::hasTopBarShadow() const {
return (_scroll->scrollTop() > 0);
}
void ContentWidget::setInnerFocus() {
if (_searchField) {
_searchField->setFocus();
} else {
_innerWrap->entity()->setFocus();
}
}
int ContentWidget::scrollTopSave() const {
return _scroll->scrollTop();
}
rpl::producer<int> ContentWidget::scrollTopValue() const {
return _scroll->scrollTopValue();
}
void ContentWidget::scrollTopRestore(int scrollTop) {
_scroll->scrollToY(scrollTop);
}
void ContentWidget::scrollTo(const Ui::ScrollToRequest &request) {
_scroll->scrollTo(request);
}
bool ContentWidget::floatPlayerHandleWheelEvent(QEvent *e) {
return _scroll->viewportEvent(e);
}
QRect ContentWidget::floatPlayerAvailableRect() const {
return mapToGlobal(_scroll->geometry());
}
void ContentWidget::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) {
const auto peer = _controller->key().peer();
const auto topic = _controller->key().topic();
const auto sublist = _controller->key().sublist();
if (!peer && !topic) {
return;
}
Window::FillDialogsEntryMenu(
_controller->parentController(),
Dialogs::EntryState{
.key = (topic
? Dialogs::Key{ topic }
: sublist
? Dialogs::Key{ sublist }
: Dialogs::Key{ peer->owner().history(peer) }),
.section = Dialogs::EntryState::Section::Profile,
},
addAction);
}
void ContentWidget::checkBeforeCloseByEscape(Fn<void()> close) {
if (_searchField) {
if (!_searchField->empty()) {
_searchField->setText({});
} else {
close();
}
} else {
close();
}
}
rpl::producer<SelectedItems> ContentWidget::selectedListValue() const {
return rpl::single(SelectedItems(Storage::SharedMediaType::Photo));
}
void ContentWidget::setPaintPadding(const style::margins &padding) {
_paintPadding = padding;
}
void ContentWidget::setViewport(
rpl::producer<not_null<QEvent*>> &&events) const {
std::move(
events
) | rpl::on_next([=](not_null<QEvent*> e) {
_scroll->viewportEvent(e);
}, _scroll->lifetime());
}
auto ContentWidget::titleStories()
-> rpl::producer<Dialogs::Stories::Content> {
return nullptr;
}
void ContentWidget::saveChanges(FnMut<void()> done) {
done();
}
void ContentWidget::refreshSearchField(bool shown) {
auto search = _controller->searchFieldController();
if (search && shown) {
auto rowView = search->createRowView(
this,
st::infoLayerMediaSearch);
_searchWrap = std::move(rowView.wrap);
_searchField = rowView.field;
const auto view = _searchWrap.get();
widthValue(
) | rpl::on_next([=](int newWidth) {
view->resizeToWidth(newWidth);
view->moveToLeft(0, 0);
}, view->lifetime());
view->show();
_searchField->setFocus();
setScrollTopSkip(view->heightNoMargins() - st::lineWidth);
} else if (_searchWrap) {
if (Ui::InFocusChain(this)) {
setFocus();
}
_searchWrap = nullptr;
setScrollTopSkip(0);
}
}
int ContentWidget::scrollBottomSkip() const {
return _scrollBottomSkip.current();
}
rpl::producer<int> ContentWidget::scrollBottomSkipValue() const {
return _scrollBottomSkip.value();
}
rpl::producer<bool> ContentWidget::desiredBottomShadowVisibility() {
using namespace rpl::mappers;
return rpl::combine(
_scroll->scrollTopValue(),
_scrollBottomSkip.value(),
_scroll->heightValue()
) | rpl::map([=](int scroll, int skip, int) {
return ((skip > 0) && (scroll < _scroll->scrollTopMax()));
});
}
not_null<Ui::ScrollArea*> ContentWidget::scroll() const {
return _scroll.data();
}
void ContentWidget::replaceSwipeHandler(
Ui::Controls::SwipeHandlerArgs *incompleteArgs) {
_swipeHandlerLifetime.destroy();
auto args = std::move(*incompleteArgs);
args.widget = _innerWrap;
args.scroll = _scroll.data();
args.onLifetime = &_swipeHandlerLifetime;
Ui::Controls::SetupSwipeHandler(std::move(args));
}
void ContentWidget::setupSwipeHandler(not_null<Ui::RpWidget*> widget) {
_swipeHandlerLifetime.destroy();
auto update = [=](Ui::Controls::SwipeContextData data) {
if (data.translation > 0) {
if (!_swipeBackData.callback) {
_swipeBackData = Ui::Controls::SetupSwipeBack(
this,
[]() -> std::pair<QColor, QColor> {
return {
st::historyForwardChooseBg->c,
st::historyForwardChooseFg->c,
};
});
}
_swipeBackData.callback(data);
return;
} else if (_swipeBackData.lifetime) {
_swipeBackData = {};
}
};
auto init = [=](int, Qt::LayoutDirection direction) {
return (direction == Qt::RightToLeft && _controller->hasBackButton())
? Ui::Controls::DefaultSwipeBackHandlerFinishData([=] {
checkBeforeClose(crl::guard(this, [=] {
_controller->parentController()->hideLayer();
_controller->showBackFromStack();
}));
})
: Ui::Controls::SwipeHandlerFinishData();
};
Ui::Controls::SetupSwipeHandler({
.widget = widget,
.scroll = _scroll.data(),
.update = std::move(update),
.init = std::move(init),
.onLifetime = &_swipeHandlerLifetime,
});
}
Key ContentMemento::key() const {
if (const auto topic = this->topic()) {
return Key(topic);
} else if (const auto sublist = this->sublist()) {
return Key(sublist);
} else if (const auto peer = this->peer()) {
return Key(peer);
} else if (const auto poll = this->poll()) {
return Key(poll, pollContextId());
} else if (const auto self = settingsSelf()) {
return Settings::Tag{ self };
} else if (const auto gifts = giftsPeer()) {
return PeerGifts::Tag{
gifts,
giftsCollectionId(),
};
} else if (const auto stories = storiesPeer()) {
return Stories::Tag{
stories,
storiesAlbumId(),
storiesAddToAlbumId(),
};
} else if (const auto music = musicPeer()) {
return Saved::MusicTag{ music };
} else if (statisticsTag().peer) {
return statisticsTag();
} else if (const auto starref = starrefPeer()) {
return BotStarRef::Tag(starref, starrefType());
} else if (const auto who = reactionsWhoReadIds()) {
return Key(who, _reactionsSelected, _pollReactionsContextId);
} else if (const auto another = globalMediaSelf()) {
return GlobalMedia::Tag{ another };
} else {
return Downloads::Tag();
}
}
ContentMemento::ContentMemento(
not_null<PeerData*> peer,
Data::ForumTopic *topic,
Data::SavedSublist *sublist,
PeerId migratedPeerId)
: _peer(peer)
, _migratedPeerId((!topic && !sublist && peer->migrateFrom())
? peer->migrateFrom()->id
: 0)
, _topic(topic)
, _sublist(sublist) {
if (_topic) {
_peer->owner().itemIdChanged(
) | rpl::on_next([=](const Data::Session::IdChange &change) {
if (_topic->rootId() == change.oldId) {
_topic = _topic->forum()->topicFor(change.newId.msg);
}
}, _lifetime);
}
}
ContentMemento::ContentMemento(Settings::Tag settings)
: _settingsSelf(settings.self.get()) {
}
ContentMemento::ContentMemento(Downloads::Tag downloads) {
}
ContentMemento::ContentMemento(Stories::Tag stories)
: _storiesPeer(stories.peer)
, _storiesAlbumId(stories.albumId)
, _storiesAddToAlbumId(stories.addingToAlbumId) {
}
ContentMemento::ContentMemento(Saved::MusicTag music)
: _musicPeer(music.peer) {
}
ContentMemento::ContentMemento(PeerGifts::Tag gifts)
: _giftsPeer(gifts.peer)
, _giftsCollectionId(gifts.collectionId) {
}
ContentMemento::ContentMemento(Statistics::Tag statistics)
: _statisticsTag(statistics) {
}
ContentMemento::ContentMemento(BotStarRef::Tag starref)
: _starrefPeer(starref.peer)
, _starrefType(starref.type) {
}
ContentMemento::ContentMemento(GlobalMedia::Tag global)
: _globalMediaSelf(global.self) {
}
ContentMemento::ContentMemento(
std::shared_ptr<Api::WhoReadList> whoReadIds,
FullMsgId contextId,
Data::ReactionId selected)
: _reactionsWhoReadIds(whoReadIds
? whoReadIds
: std::make_shared<Api::WhoReadList>())
, _reactionsSelected(selected)
, _pollReactionsContextId(contextId) {
}
} // namespace Info

View File

@@ -0,0 +1,386 @@
/*
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 "info/info_flexible_scroll.h"
#include "info/info_wrap_widget.h"
#include "info/statistics/info_statistics_tag.h"
#include "ui/controls/swipe_handler_data.h"
namespace Api {
struct WhoReadList;
} // namespace Api
namespace Dialogs::Stories {
struct Content;
} // namespace Dialogs::Stories
namespace Storage {
enum class SharedMediaType : signed char;
} // namespace Storage
namespace Ui {
namespace Controls {
struct SwipeHandlerArgs;
} // namespace Controls
class RoundRect;
class ScrollArea;
class InputField;
struct ScrollToRequest;
template <typename Widget>
class PaddingWrap;
} // namespace Ui
namespace Ui::Menu {
struct MenuCallback;
} // namespace Ui::Menu
namespace Info::Settings {
struct Tag;
} // namespace Info::Settings
namespace Info::Downloads {
struct Tag;
} // namespace Info::Downloads
namespace Info::Statistics {
struct Tag;
} // namespace Info::Statistics
namespace Info::BotStarRef {
enum class Type : uchar;
struct Tag;
} // namespace Info::BotStarRef
namespace Info::GlobalMedia {
struct Tag;
} // namespace Info::GlobalMedia
namespace Info::PeerGifts {
struct Tag;
} // namespace Info::PeerGifts
namespace Info::Stories {
struct Tag;
} // namespace Info::Stories
namespace Info::Saved {
struct MusicTag;
} // namespace Info::Saved
namespace Info {
class ContentMemento;
class Controller;
struct FlexibleScrollData;
class ContentWidget : public Ui::RpWidget {
public:
ContentWidget(
QWidget *parent,
not_null<Controller*> controller);
virtual bool showInternal(
not_null<ContentMemento*> memento) = 0;
std::shared_ptr<ContentMemento> createMemento();
virtual void setIsStackBottom(bool isStackBottom);
[[nodiscard]] bool isStackBottom() const;
rpl::producer<int> scrollHeightValue() const;
rpl::producer<int> desiredHeightValue() const override;
virtual rpl::producer<bool> desiredShadowVisibility() const;
bool hasTopBarShadow() const;
virtual void setInnerFocus();
virtual void showFinished() {
}
virtual void enableBackButton() {
}
// When resizing the widget with top edge moved up or down and we
// want to add this top movement to the scroll position, so inner
// content will not move.
void setGeometryWithTopMoved(
const QRect &newGeometry,
int topDelta);
void applyAdditionalScroll(int additionalScroll);
void applyMaxVisibleHeight(int maxVisibleHeight);
int scrollTillBottom(int forHeight) const;
[[nodiscard]] rpl::producer<int> scrollTillBottomChanges() const;
[[nodiscard]] virtual const Ui::RoundRect *bottomSkipRounding() const {
return nullptr;
}
// Float player interface.
bool floatPlayerHandleWheelEvent(QEvent *e);
QRect floatPlayerAvailableRect() const;
virtual rpl::producer<SelectedItems> selectedListValue() const;
virtual void selectionAction(SelectionAction action) {
}
virtual void fillTopBarMenu(const Ui::Menu::MenuCallback &addAction);
[[nodiscard]] virtual bool closeByOutsideClick() const {
return true;
}
virtual void checkBeforeClose(Fn<void()> close) {
close();
}
virtual void checkBeforeCloseByEscape(Fn<void()> close);
[[nodiscard]] virtual rpl::producer<QString> title() = 0;
[[nodiscard]] virtual rpl::producer<QString> subtitle() {
return nullptr;
}
[[nodiscard]] virtual auto titleStories()
-> rpl::producer<Dialogs::Stories::Content>;
virtual void saveChanges(FnMut<void()> done);
[[nodiscard]] int scrollBottomSkip() const;
[[nodiscard]] rpl::producer<int> scrollBottomSkipValue() const;
[[nodiscard]] virtual auto desiredBottomShadowVisibility()
-> rpl::producer<bool>;
void replaceSwipeHandler(Ui::Controls::SwipeHandlerArgs *incompleteArgs);
protected:
template <typename Widget>
Widget *setInnerWidget(object_ptr<Widget> inner) {
return static_cast<Widget*>(
doSetInnerWidget(std::move(inner)));
}
template <typename Widget>
Widget *setupFlexibleInnerWidget(
object_ptr<Widget> inner,
FlexibleScrollData &flexibleScroll,
Fn<void(Ui::RpWidget*)> customSetup = nullptr) {
if (!inner->hasFlexibleTopBar()) {
return setInnerWidget(std::move(inner));
}
return static_cast<Widget*>(doSetupFlexibleInnerWidget(
std::move(inner),
flexibleScroll,
std::move(customSetup)));
}
[[nodiscard]] not_null<Controller*> controller() const {
return _controller;
}
[[nodiscard]] not_null<Ui::ScrollArea*> scroll() const;
[[nodiscard]] int maxVisibleHeight() const {
return _maxVisibleHeight;
}
void resizeEvent(QResizeEvent *e) override;
void paintEvent(QPaintEvent *e) override;
void setScrollTopSkip(int scrollTopSkip);
void setScrollBottomSkip(int scrollBottomSkip);
int scrollTopSave() const;
void scrollTopRestore(int scrollTop);
void scrollTo(const Ui::ScrollToRequest &request);
[[nodiscard]] rpl::producer<int> scrollTopValue() const;
void setPaintPadding(const style::margins &padding);
void setViewport(rpl::producer<not_null<QEvent*>> &&events) const;
private:
Ui::RpWidget *doSetInnerWidget(object_ptr<Ui::RpWidget> inner);
Ui::RpWidget *doSetupFlexibleInnerWidget(
object_ptr<Ui::RpWidget> inner,
FlexibleScrollData &flexibleScroll,
Fn<void(Ui::RpWidget*)> customSetup);
void updateControlsGeometry();
void refreshSearchField(bool shown);
void setupSwipeHandler(not_null<Ui::RpWidget*> widget);
void updateInnerPadding();
virtual std::shared_ptr<ContentMemento> doCreateMemento() = 0;
const not_null<Controller*> _controller;
style::color _bg;
rpl::variable<int> _scrollTopSkip = -1;
rpl::variable<int> _scrollBottomSkip = 0;
rpl::event_stream<int> _scrollTillBottomChanges;
object_ptr<Ui::ScrollArea> _scroll;
Ui::PaddingWrap<Ui::RpWidget> *_innerWrap = nullptr;
base::unique_qptr<Ui::RpWidget> _searchWrap = nullptr;
QPointer<Ui::InputField> _searchField;
int _innerDesiredHeight = 0;
int _additionalScroll = 0;
int _addedHeight = 0;
int _maxVisibleHeight = 0;
bool _isStackBottom = false;
// Saving here topDelta in setGeometryWithTopMoved() to get it passed to resizeEvent().
int _topDelta = 0;
// To paint round edges from content.
style::margins _paintPadding;
Ui::Controls::SwipeBackResult _swipeBackData;
rpl::lifetime _swipeHandlerLifetime;
};
class ContentMemento {
public:
ContentMemento(
not_null<PeerData*> peer,
Data::ForumTopic *topic,
Data::SavedSublist *sublist,
PeerId migratedPeerId);
explicit ContentMemento(PeerGifts::Tag gifts);
explicit ContentMemento(Settings::Tag settings);
explicit ContentMemento(Downloads::Tag downloads);
explicit ContentMemento(Stories::Tag stories);
explicit ContentMemento(Saved::MusicTag music);
explicit ContentMemento(Statistics::Tag statistics);
explicit ContentMemento(BotStarRef::Tag starref);
explicit ContentMemento(GlobalMedia::Tag global);
ContentMemento(not_null<PollData*> poll, FullMsgId contextId)
: _poll(poll)
, _pollReactionsContextId(contextId) {
}
ContentMemento(
std::shared_ptr<Api::WhoReadList> whoReadIds,
FullMsgId contextId,
Data::ReactionId selected);
virtual ~ContentMemento() = default;
[[nodiscard]] virtual object_ptr<ContentWidget> createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) = 0;
[[nodiscard]] PeerData *peer() const {
return _peer;
}
[[nodiscard]] PeerId migratedPeerId() const {
return _migratedPeerId;
}
[[nodiscard]] Data::ForumTopic *topic() const {
return _topic;
}
[[nodiscard]] Data::SavedSublist *sublist() const {
return _sublist;
}
[[nodiscard]] UserData *settingsSelf() const {
return _settingsSelf;
}
[[nodiscard]] PeerData *storiesPeer() const {
return _storiesPeer;
}
[[nodiscard]] int storiesAlbumId() const {
return _storiesAlbumId;
}
[[nodiscard]] int storiesAddToAlbumId() const {
return _storiesAddToAlbumId;
}
[[nodiscard]] PeerData *musicPeer() const {
return _musicPeer;
}
[[nodiscard]] PeerData *giftsPeer() const {
return _giftsPeer;
}
[[nodiscard]] int giftsCollectionId() const {
return _giftsCollectionId;
}
[[nodiscard]] Statistics::Tag statisticsTag() const {
return _statisticsTag;
}
[[nodiscard]] PeerData *starrefPeer() const {
return _starrefPeer;
}
[[nodiscard]] BotStarRef::Type starrefType() const {
return _starrefType;
}
[[nodiscard]] PollData *poll() const {
return _poll;
}
[[nodiscard]] FullMsgId pollContextId() const {
return _poll ? _pollReactionsContextId : FullMsgId();
}
[[nodiscard]] auto reactionsWhoReadIds() const
-> std::shared_ptr<Api::WhoReadList> {
return _reactionsWhoReadIds;
}
[[nodiscard]] Data::ReactionId reactionsSelected() const {
return _reactionsSelected;
}
[[nodiscard]] FullMsgId reactionsContextId() const {
return _reactionsWhoReadIds ? _pollReactionsContextId : FullMsgId();
}
[[nodiscard]] UserData *globalMediaSelf() const {
return _globalMediaSelf;
}
[[nodiscard]] Key key() const;
[[nodiscard]] virtual Section section() const = 0;
void setScrollTop(int scrollTop) {
_scrollTop = scrollTop;
}
int scrollTop() const {
return _scrollTop;
}
void setSearchFieldQuery(const QString &query) {
_searchFieldQuery = query;
}
[[nodiscard]] QString searchFieldQuery() const {
return _searchFieldQuery;
}
void setSearchEnabledByContent(bool enabled) {
_searchEnabledByContent = enabled;
}
[[nodiscard]] bool searchEnabledByContent() const {
return _searchEnabledByContent;
}
void setSearchStartsFocused(bool focused) {
_searchStartsFocused = focused;
}
[[nodiscard]] bool searchStartsFocused() const {
return _searchStartsFocused;
}
private:
PeerData * const _peer = nullptr;
const PeerId _migratedPeerId = 0;
Data::ForumTopic *_topic = nullptr;
Data::SavedSublist *_sublist = nullptr;
UserData * const _settingsSelf = nullptr;
PeerData * const _storiesPeer = nullptr;
int _storiesAlbumId = 0;
int _storiesAddToAlbumId = 0;
PeerData * const _musicPeer = nullptr;
PeerData * const _giftsPeer = nullptr;
int _giftsCollectionId = 0;
Statistics::Tag _statisticsTag;
PeerData * const _starrefPeer = nullptr;
BotStarRef::Type _starrefType = {};
PollData * const _poll = nullptr;
std::shared_ptr<Api::WhoReadList> _reactionsWhoReadIds;
Data::ReactionId _reactionsSelected;
const FullMsgId _pollReactionsContextId;
UserData * const _globalMediaSelf = nullptr;
int _scrollTop = 0;
QString _searchFieldQuery;
bool _searchEnabledByContent = false;
bool _searchStartsFocused = false;
rpl::lifetime _lifetime;
};
} // namespace Info

View File

@@ -0,0 +1,563 @@
/*
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 "info/info_controller.h"
#include "ui/search_field_controller.h"
#include "history/history.h"
#include "info/info_content_widget.h"
#include "info/info_memento.h"
#include "info/global_media/info_global_media_widget.h"
#include "info/media/info_media_widget.h"
#include "core/application.h"
#include "data/data_changes.h"
#include "data/data_peer.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_forum_topic.h"
#include "data/data_forum.h"
#include "data/data_saved_sublist.h"
#include "data/data_session.h"
#include "data/data_shared_media.h"
#include "data/data_media_types.h"
#include "data/data_download_manager.h"
#include "history/history_item.h"
#include "main/main_session.h"
#include "window/window_session_controller.h"
namespace Info {
Key::Key(not_null<PeerData*> peer) : _value(peer) {
}
Key::Key(not_null<Data::ForumTopic*> topic) : _value(topic) {
}
Key::Key(not_null<Data::SavedSublist*> sublist) : _value(sublist) {
}
Key::Key(Settings::Tag settings) : _value(settings) {
}
Key::Key(Downloads::Tag downloads) : _value(downloads) {
}
Key::Key(Stories::Tag stories) : _value(stories) {
}
Key::Key(Saved::MusicTag music) : _value(music) {
}
Key::Key(Statistics::Tag statistics) : _value(statistics) {
}
Key::Key(PeerGifts::Tag gifts) : _value(gifts) {
}
Key::Key(BotStarRef::Tag starref) : _value(starref) {
}
Key::Key(GlobalMedia::Tag global) : _value(global) {
}
Key::Key(not_null<PollData*> poll, FullMsgId contextId)
: _value(PollKey{ poll, contextId }) {
}
Key::Key(
std::shared_ptr<Api::WhoReadList> whoReadIds,
Data::ReactionId selected,
FullMsgId contextId)
: _value(ReactionsKey{ whoReadIds, selected, contextId }) {
}
PeerData *Key::peer() const {
if (const auto peer = std::get_if<not_null<PeerData*>>(&_value)) {
return *peer;
} else if (const auto topic = this->topic()) {
return topic->peer();
} else if (const auto sublist = this->sublist()) {
return sublist->owningHistory()->peer;
}
return nullptr;
}
Data::ForumTopic *Key::topic() const {
if (const auto topic = std::get_if<not_null<Data::ForumTopic*>>(
&_value)) {
return *topic;
}
return nullptr;
}
Data::SavedSublist *Key::sublist() const {
if (const auto sublist = std::get_if<not_null<Data::SavedSublist*>>(
&_value)) {
return *sublist;
}
return nullptr;
}
UserData *Key::settingsSelf() const {
if (const auto tag = std::get_if<Settings::Tag>(&_value)) {
return tag->self;
}
return nullptr;
}
bool Key::isDownloads() const {
return v::is<Downloads::Tag>(_value);
}
bool Key::isGlobalMedia() const {
return v::is<GlobalMedia::Tag>(_value);
}
PeerData *Key::storiesPeer() const {
if (const auto tag = std::get_if<Stories::Tag>(&_value)) {
return tag->peer;
}
return nullptr;
}
int Key::storiesAlbumId() const {
if (const auto tag = std::get_if<Stories::Tag>(&_value)) {
return tag->albumId;
}
return 0;
}
int Key::storiesAddToAlbumId() const {
if (const auto tag = std::get_if<Stories::Tag>(&_value)) {
return tag->addingToAlbumId;
}
return 0;
}
PeerData *Key::musicPeer() const {
if (const auto tag = std::get_if<Saved::MusicTag>(&_value)) {
return tag->peer;
}
return nullptr;
}
PeerData *Key::giftsPeer() const {
if (const auto tag = std::get_if<PeerGifts::Tag>(&_value)) {
return tag->peer;
}
return nullptr;
}
int Key::giftsCollectionId() const {
if (const auto tag = std::get_if<PeerGifts::Tag>(&_value)) {
return tag->collectionId;
}
return 0;
}
Statistics::Tag Key::statisticsTag() const {
if (const auto tag = std::get_if<Statistics::Tag>(&_value)) {
return *tag;
}
return Statistics::Tag();
}
PeerData *Key::starrefPeer() const {
if (const auto tag = std::get_if<BotStarRef::Tag>(&_value)) {
return tag->peer;
}
return nullptr;
}
BotStarRef::Type Key::starrefType() const {
if (const auto tag = std::get_if<BotStarRef::Tag>(&_value)) {
return tag->type;
}
return BotStarRef::Type();
}
PollData *Key::poll() const {
if (const auto data = std::get_if<PollKey>(&_value)) {
return data->poll;
}
return nullptr;
}
FullMsgId Key::pollContextId() const {
if (const auto data = std::get_if<PollKey>(&_value)) {
return data->contextId;
}
return FullMsgId();
}
std::shared_ptr<Api::WhoReadList> Key::reactionsWhoReadIds() const {
if (const auto data = std::get_if<ReactionsKey>(&_value)) {
return data->whoReadIds;
}
return nullptr;
}
Data::ReactionId Key::reactionsSelected() const {
if (const auto data = std::get_if<ReactionsKey>(&_value)) {
return data->selected;
}
return Data::ReactionId();
}
FullMsgId Key::reactionsContextId() const {
if (const auto data = std::get_if<ReactionsKey>(&_value)) {
return data->contextId;
}
return FullMsgId();
}
rpl::producer<SparseIdsMergedSlice> AbstractController::mediaSource(
SparseIdsMergedSlice::UniversalMsgId aroundId,
int limitBefore,
int limitAfter) const {
Expects(peer() != nullptr);
const auto isScheduled = [&] {
const auto peerId = peer()->id;
if (const auto item = session().data().message(peerId, aroundId)) {
return item->isScheduled();
}
return false;
}();
const auto mediaViewer = isScheduled
? SharedScheduledMediaViewer
: SharedMediaMergedViewer;
const auto topicId = isScheduled
? SparseIdsMergedSlice::kScheduledTopicId
: topic()
? topic()->rootId()
: MsgId(0);
return mediaViewer(
&session(),
SharedMediaMergedKey(
SparseIdsMergedSlice::Key(
peer()->id,
topicId,
sublist() ? sublist()->sublistPeer()->id : PeerId(),
migratedPeerId(),
aroundId),
section().mediaType()),
limitBefore,
limitAfter);
}
rpl::producer<QString> AbstractController::mediaSourceQueryValue() const {
return rpl::single(QString());
}
rpl::producer<QString> AbstractController::searchQueryValue() const {
return rpl::single(QString());
}
AbstractController::AbstractController(
not_null<Window::SessionController*> parent)
: SessionNavigation(&parent->session())
, _parent(parent) {
}
PeerData *AbstractController::peer() const {
return key().peer();
}
PeerId AbstractController::migratedPeerId() const {
if (const auto peer = migrated()) {
return peer->id;
}
return PeerId(0);
}
PollData *AbstractController::poll() const {
if (const auto item = session().data().message(pollContextId())) {
if (const auto media = item->media()) {
return media->poll();
}
}
return nullptr;
}
auto AbstractController::reactionsWhoReadIds() const
-> std::shared_ptr<Api::WhoReadList> {
return key().reactionsWhoReadIds();
}
Data::ReactionId AbstractController::reactionsSelected() const {
return key().reactionsSelected();
}
FullMsgId AbstractController::reactionsContextId() const {
return key().reactionsContextId();
}
void AbstractController::showSection(
std::shared_ptr<Window::SectionMemento> memento,
const Window::SectionShow &params) {
return parentController()->showSection(std::move(memento), params);
}
void AbstractController::showBackFromStack(
const Window::SectionShow &params) {
return parentController()->showBackFromStack(params);
}
void AbstractController::showPeerHistory(
PeerId peerId,
const Window::SectionShow &params,
MsgId msgId) {
return parentController()->showPeerHistory(peerId, params, msgId);
}
Controller::Controller(
not_null<WrapWidget*> widget,
not_null<Window::SessionController*> window,
not_null<ContentMemento*> memento)
: AbstractController(window)
, _widget(widget)
, _key(memento->key())
, _migrated(memento->migratedPeerId()
? window->session().data().peer(memento->migratedPeerId()).get()
: nullptr)
, _section(memento->section()) {
updateSearchControllers(memento);
setupMigrationViewer();
setupTopicViewer();
}
void Controller::replaceKey(Key key) {
_key = key;
}
void Controller::setupMigrationViewer() {
const auto peer = _key.peer();
if (_key.topic()
|| !peer
|| (!peer->isChat() && !peer->isChannel())
|| _migrated) {
return;
}
peer->session().changes().peerFlagsValue(
peer,
Data::PeerUpdate::Flag::Migration
) | rpl::filter([=] {
return peer->migrateTo() || (peer->migrateFrom() != _migrated);
}) | rpl::on_next([=] {
replaceWith(std::make_shared<Memento>(peer, _section));
}, lifetime());
}
void Controller::replaceWith(std::shared_ptr<Memento> memento) {
const auto window = parentController();
auto params = Window::SectionShow(
Window::SectionShow::Way::Backward,
anim::type::instant,
anim::activation::background);
if (wrap() == Wrap::Side) {
params.thirdColumn = true;
}
InvokeQueued(_widget, [=, memento = std::move(memento)]() mutable {
window->showSection(std::move(memento), params);
});
}
void Controller::setupTopicViewer() {
session().data().itemIdChanged(
) | rpl::on_next([=](const Data::Session::IdChange &change) {
if (const auto topic = _key.topic()) {
if (topic->rootId() == change.oldId
|| (topic->peer()->id == change.newId.peer
&& topic->rootId() == change.newId.msg)) {
const auto now = topic->forum()->topicFor(change.newId.msg);
_key = Key(now);
replaceWith(std::make_shared<Memento>(now, _section));
}
}
}, _lifetime);
}
Wrap Controller::wrap() const {
return _widget->wrap();
}
rpl::producer<Wrap> Controller::wrapValue() const {
return _widget->wrapValue();
}
not_null<Ui::RpWidget*> Controller::wrapWidget() const {
return _widget;
}
bool Controller::validateMementoPeer(
not_null<ContentMemento*> memento) const {
return memento->peer() == peer()
&& memento->migratedPeerId() == migratedPeerId()
&& memento->settingsSelf() == settingsSelf()
&& memento->storiesPeer() == storiesPeer()
&& memento->musicPeer() == musicPeer()
&& memento->statisticsTag().peer == statisticsTag().peer
&& memento->starrefPeer() == starrefPeer()
&& memento->starrefType() == starrefType();
}
void Controller::setSection(not_null<ContentMemento*> memento) {
_section = memento->section();
updateSearchControllers(memento);
}
bool Controller::hasBackButton() const {
return _widget->hasBackButton();
}
void Controller::updateSearchControllers(
not_null<ContentMemento*> memento) {
using Type = Section::Type;
const auto type = _section.type();
const auto isMedia = (type == Type::Media)
|| (type == Type::GlobalMedia);
const auto mediaType = isMedia
? _section.mediaType()
: Section::MediaType::kCount;
const auto hasMediaSearch = isMedia
&& SharedMediaAllowSearch(mediaType);
const auto hasRequestsListSearch = (type == Type::RequestsList);
const auto hasCommonGroupsSearch = (type == Type::CommonGroups);
const auto hasDownloadsSearch = (type == Type::Downloads);
const auto hasMembersSearch = (type == Type::Members)
|| (type == Type::Profile);
const auto searchQuery = memento->searchFieldQuery();
if (type == Type::Media) {
_searchController
= std::make_unique<Api::DelayedSearchController>(&session());
auto mediaMemento = dynamic_cast<Media::Memento*>(memento.get());
Assert(mediaMemento != nullptr);
_searchController->restoreState(mediaMemento->searchState());
} else {
_searchController = nullptr;
}
if (hasMediaSearch
|| hasRequestsListSearch
|| hasCommonGroupsSearch
|| hasDownloadsSearch
|| hasMembersSearch) {
_searchFieldController
= std::make_unique<Ui::SearchFieldController>(
searchQuery);
if (_searchController) {
_searchFieldController->queryValue(
) | rpl::on_next([=](QString &&query) {
_searchController->setQuery(
produceSearchQuery(std::move(query)));
}, _searchFieldController->lifetime());
}
_seachEnabledByContent = memento->searchEnabledByContent();
_searchStartsFocused = memento->searchStartsFocused();
} else {
_searchFieldController = nullptr;
}
}
void Controller::saveSearchState(not_null<ContentMemento*> memento) {
if (_searchFieldController) {
memento->setSearchFieldQuery(
_searchFieldController->query());
memento->setSearchEnabledByContent(
_seachEnabledByContent.current());
}
if (_searchController) {
auto mediaMemento = dynamic_cast<Media::Memento*>(
memento.get());
Assert(mediaMemento != nullptr);
mediaMemento->setSearchState(_searchController->saveState());
}
}
void Controller::showSection(
std::shared_ptr<Window::SectionMemento> memento,
const Window::SectionShow &params) {
if (!_widget->showInternal(memento.get(), params)) {
AbstractController::showSection(std::move(memento), params);
}
}
void Controller::showBackFromStack(const Window::SectionShow &params) {
if (!_widget->showBackFromStackInternal(params)) {
AbstractController::showBackFromStack(params);
}
}
void Controller::removeFromStack(const std::vector<Section> &sections) const {
_widget->removeFromStack(sections);
}
auto Controller::produceSearchQuery(
const QString &query) const -> SearchQuery {
Expects(_key.peer() != nullptr);
auto result = SearchQuery();
result.type = _section.mediaType();
result.peerId = _key.peer()->id;
result.topicRootId = _key.topic() ? _key.topic()->rootId() : 0;
result.query = query;
result.migratedPeerId = _migrated ? _migrated->id : PeerId(0);
return result;
}
rpl::producer<bool> Controller::searchEnabledByContent() const {
return _seachEnabledByContent.value();
}
rpl::producer<QString> Controller::mediaSourceQueryValue() const {
return _searchController->currentQueryValue();
}
rpl::producer<QString> Controller::searchQueryValue() const {
const auto controller = searchFieldController();
return controller ? controller->queryValue() : rpl::single(QString());
}
rpl::producer<SparseIdsMergedSlice> Controller::mediaSource(
SparseIdsMergedSlice::UniversalMsgId aroundId,
int limitBefore,
int limitAfter) const {
auto query = _searchController->currentQuery();
if (!query.query.isEmpty()) {
return _searchController->idsSlice(
aroundId,
limitBefore,
limitAfter);
}
return SharedMediaMergedViewer(
&session(),
SharedMediaMergedKey(
SparseIdsMergedSlice::Key(
query.peerId,
query.topicRootId,
query.monoforumPeerId,
query.migratedPeerId,
aroundId),
query.type),
limitBefore,
limitAfter);
}
std::any &Controller::stepDataReference() {
return _stepData;
}
void Controller::takeStepData(not_null<Controller*> another) {
_stepData = base::take(another->_stepData);
}
Controller::~Controller() = default;
} // namespace Info

View File

@@ -0,0 +1,394 @@
/*
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 "data/data_message_reaction_id.h"
#include "data/data_search_controller.h"
#include "info/peer_gifts/info_peer_gifts_common.h"
#include "info/saved/info_saved_music_common.h"
#include "info/statistics/info_statistics_tag.h"
#include "info/stories/info_stories_common.h"
#include "window/window_session_controller.h"
namespace Api {
struct WhoReadList;
} // namespace Api
namespace Data {
class ForumTopic;
class SavedSublist;
} // namespace Data
namespace Ui {
class SearchFieldController;
} // namespace Ui
namespace Info::Settings {
struct Tag {
explicit Tag(not_null<UserData*> self) : self(self) {
}
not_null<UserData*> self;
};
} // namespace Info::Settings
namespace Info::Downloads {
struct Tag {
};
} // namespace Info::Downloads
namespace Info::GlobalMedia {
struct Tag {
explicit Tag(not_null<UserData*> self) : self(self) {
}
not_null<UserData*> self;
};
} // namespace Info::GlobalMedia
namespace Info::BotStarRef {
enum class Type : uchar {
Setup,
Join,
};
struct Tag {
Tag(not_null<PeerData*> peer, Type type) : peer(peer), type(type) {
}
not_null<PeerData*> peer;
Type type = {};
};
} // namespace Info::BotStarRef
namespace Info {
class Key {
public:
explicit Key(not_null<PeerData*> peer);
explicit Key(not_null<Data::ForumTopic*> topic);
explicit Key(not_null<Data::SavedSublist*> sublist);
Key(Settings::Tag settings);
Key(Downloads::Tag downloads);
Key(Stories::Tag stories);
Key(Saved::MusicTag music);
Key(Statistics::Tag statistics);
Key(PeerGifts::Tag gifts);
Key(BotStarRef::Tag starref);
Key(GlobalMedia::Tag global);
Key(not_null<PollData*> poll, FullMsgId contextId);
Key(
std::shared_ptr<Api::WhoReadList> whoReadIds,
Data::ReactionId selected,
FullMsgId contextId);
[[nodiscard]] PeerData *peer() const;
[[nodiscard]] Data::ForumTopic *topic() const;
[[nodiscard]] Data::SavedSublist *sublist() const;
[[nodiscard]] UserData *settingsSelf() const;
[[nodiscard]] bool isDownloads() const;
[[nodiscard]] bool isGlobalMedia() const;
[[nodiscard]] PeerData *storiesPeer() const;
[[nodiscard]] int storiesAlbumId() const;
[[nodiscard]] int storiesAddToAlbumId() const;
[[nodiscard]] PeerData *musicPeer() const;
[[nodiscard]] PeerData *giftsPeer() const;
[[nodiscard]] int giftsCollectionId() const;
[[nodiscard]] Statistics::Tag statisticsTag() const;
[[nodiscard]] PeerData *starrefPeer() const;
[[nodiscard]] BotStarRef::Type starrefType() const;
[[nodiscard]] PollData *poll() const;
[[nodiscard]] FullMsgId pollContextId() const;
[[nodiscard]] auto reactionsWhoReadIds() const
-> std::shared_ptr<Api::WhoReadList>;
[[nodiscard]] Data::ReactionId reactionsSelected() const;
[[nodiscard]] FullMsgId reactionsContextId() const;
private:
struct PollKey {
not_null<PollData*> poll;
FullMsgId contextId;
};
struct ReactionsKey {
std::shared_ptr<Api::WhoReadList> whoReadIds;
Data::ReactionId selected;
FullMsgId contextId;
};
std::variant<
not_null<PeerData*>,
not_null<Data::ForumTopic*>,
not_null<Data::SavedSublist*>,
Settings::Tag,
Downloads::Tag,
Stories::Tag,
Saved::MusicTag,
Statistics::Tag,
PeerGifts::Tag,
BotStarRef::Tag,
GlobalMedia::Tag,
PollKey,
ReactionsKey> _value;
};
enum class Wrap;
class WrapWidget;
class Memento;
class ContentMemento;
class Section final {
public:
enum class Type {
Profile,
Media,
GlobalMedia,
CommonGroups,
SimilarPeers,
RequestsList,
ReactionsList,
SavedSublists,
PeerGifts,
Members,
Settings,
Downloads,
Stories,
SavedMusic,
PollResults,
Statistics,
BotStarRef,
Boosts,
ChannelEarn,
BotEarn,
};
using SettingsType = ::Settings::Type;
using MediaType = Storage::SharedMediaType;
Section(Type type) : _type(type) {
Expects(type != Type::Media
&& type != Type::GlobalMedia
&& type != Type::Settings);
}
Section(MediaType mediaType, Type type = Type::Media)
: _type(type)
, _mediaType(mediaType) {
}
Section(SettingsType settingsType)
: _type(Type::Settings)
, _settingsType(settingsType) {
}
[[nodiscard]] Type type() const {
return _type;
}
[[nodiscard]] MediaType mediaType() const {
Expects(_type == Type::Media || _type == Type::GlobalMedia);
return _mediaType;
}
[[nodiscard]] SettingsType settingsType() const {
Expects(_type == Type::Settings);
return _settingsType;
}
private:
Type _type;
MediaType _mediaType = MediaType();
SettingsType _settingsType = SettingsType();
};
class AbstractController : public Window::SessionNavigation {
public:
AbstractController(not_null<Window::SessionController*> parent);
[[nodiscard]] virtual Key key() const = 0;
[[nodiscard]] virtual PeerData *migrated() const = 0;
[[nodiscard]] virtual Section section() const = 0;
[[nodiscard]] PeerData *peer() const;
[[nodiscard]] PeerId migratedPeerId() const;
[[nodiscard]] Data::ForumTopic *topic() const {
return key().topic();
}
[[nodiscard]] Data::SavedSublist *sublist() const {
return key().sublist();
}
[[nodiscard]] UserData *settingsSelf() const {
return key().settingsSelf();
}
[[nodiscard]] bool isDownloads() const {
return key().isDownloads();
}
[[nodiscard]] bool isGlobalMedia() const {
return key().isGlobalMedia();
}
[[nodiscard]] PeerData *storiesPeer() const {
return key().storiesPeer();
}
[[nodiscard]] int storiesAlbumId() const {
return key().storiesAlbumId();
}
[[nodiscard]] int storiesAddToAlbumId() const {
return key().storiesAddToAlbumId();
}
[[nodiscard]] PeerData *musicPeer() const {
return key().musicPeer();
}
[[nodiscard]] PeerData *giftsPeer() const {
return key().giftsPeer();
}
[[nodiscard]] int giftsCollectionId() const {
return key().giftsCollectionId();
}
[[nodiscard]] Statistics::Tag statisticsTag() const {
return key().statisticsTag();
}
[[nodiscard]] PeerData *starrefPeer() const {
return key().starrefPeer();
}
[[nodiscard]] BotStarRef::Type starrefType() const {
return key().starrefType();
}
[[nodiscard]] PollData *poll() const;
[[nodiscard]] FullMsgId pollContextId() const {
return key().pollContextId();
}
[[nodiscard]] auto reactionsWhoReadIds() const
-> std::shared_ptr<Api::WhoReadList>;
[[nodiscard]] Data::ReactionId reactionsSelected() const;
[[nodiscard]] FullMsgId reactionsContextId() const;
virtual void setSearchEnabledByContent(bool enabled) {
}
virtual rpl::producer<SparseIdsMergedSlice> mediaSource(
SparseIdsMergedSlice::UniversalMsgId aroundId,
int limitBefore,
int limitAfter) const;
virtual rpl::producer<QString> mediaSourceQueryValue() const;
virtual rpl::producer<QString> searchQueryValue() const;
void showSection(
std::shared_ptr<Window::SectionMemento> memento,
const Window::SectionShow &params = Window::SectionShow()) override;
void showBackFromStack(
const Window::SectionShow &params = Window::SectionShow()) override;
void showPeerHistory(
PeerId peerId,
const Window::SectionShow &params = Window::SectionShow::Way::ClearStack,
MsgId msgId = ShowAtUnreadMsgId) override;
not_null<Window::SessionController*> parentController() override {
return _parent;
}
private:
not_null<Window::SessionController*> _parent;
};
class Controller : public AbstractController {
public:
Controller(
not_null<WrapWidget*> widget,
not_null<Window::SessionController*> window,
not_null<ContentMemento*> memento);
Key key() const override {
return _key;
}
PeerData *migrated() const override {
return _migrated;
}
Section section() const override {
return _section;
}
void replaceKey(Key key);
[[nodiscard]] bool validateMementoPeer(
not_null<ContentMemento*> memento) const;
[[nodiscard]] Wrap wrap() const;
[[nodiscard]] rpl::producer<Wrap> wrapValue() const;
[[nodiscard]] not_null<Ui::RpWidget*> wrapWidget() const;
void setSection(not_null<ContentMemento*> memento);
[[nodiscard]] bool hasBackButton() const;
Ui::SearchFieldController *searchFieldController() const {
return _searchFieldController.get();
}
void setSearchEnabledByContent(bool enabled) override {
_seachEnabledByContent = enabled;
}
rpl::producer<bool> searchEnabledByContent() const;
rpl::producer<SparseIdsMergedSlice> mediaSource(
SparseIdsMergedSlice::UniversalMsgId aroundId,
int limitBefore,
int limitAfter) const override;
rpl::producer<QString> mediaSourceQueryValue() const override;
rpl::producer<QString> searchQueryValue() const override;
bool takeSearchStartsFocused() {
return base::take(_searchStartsFocused);
}
void saveSearchState(not_null<ContentMemento*> memento);
void showSection(
std::shared_ptr<Window::SectionMemento> memento,
const Window::SectionShow &params = Window::SectionShow()) override;
void showBackFromStack(
const Window::SectionShow &params = Window::SectionShow()) override;
void removeFromStack(const std::vector<Section> &sections) const;
void takeStepData(not_null<Controller*> another);
std::any &stepDataReference();
rpl::lifetime &lifetime() {
return _lifetime;
}
~Controller();
private:
using SearchQuery = Api::DelayedSearchController::Query;
void updateSearchControllers(not_null<ContentMemento*> memento);
SearchQuery produceSearchQuery(const QString &query) const;
void setupMigrationViewer();
void setupTopicViewer();
void replaceWith(std::shared_ptr<Memento> memento);
not_null<WrapWidget*> _widget;
Key _key;
PeerData *_migrated = nullptr;
rpl::variable<Wrap> _wrap;
Section _section;
std::unique_ptr<Ui::SearchFieldController> _searchFieldController;
std::unique_ptr<Api::DelayedSearchController> _searchController;
rpl::variable<bool> _seachEnabledByContent = false;
bool _searchStartsFocused = false;
// Data between sections based on steps.
std::any _stepData;
rpl::lifetime _lifetime;
};
} // namespace Info

View File

@@ -0,0 +1,361 @@
/*
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 "info/info_flexible_scroll.h"
#include "ui/effects/animation_value.h"
#include "ui/widgets/scroll_area.h"
#include "base/event_filter.h"
#include "base/options.h"
#include "styles/style_info.h"
#include <QtWidgets/QApplication>
#include <QtWidgets/QScrollBar>
namespace Info {
base::options::toggle AlternativeScrollProcessing({
.id = kAlternativeScrollProcessing,
.name = "Use legacy scroll processing in profiles.",
});
const char kAlternativeScrollProcessing[] = "alternative-scroll-processing";
FlexibleScrollHelper::FlexibleScrollHelper(
not_null<Ui::ScrollArea*> scroll,
not_null<Ui::RpWidget*> inner,
not_null<Ui::RpWidget*> pinnedToTop,
Fn<void(QMargins)> setPaintPadding,
Fn<void(rpl::producer<not_null<QEvent*>>&&)> setViewport,
FlexibleScrollData &data)
: _scroll(scroll)
, _inner(inner)
, _pinnedToTop(pinnedToTop)
, _setPaintPadding(setPaintPadding)
, _setViewport(setViewport)
, _data(data) {
setupScrollAnimation();
if (AlternativeScrollProcessing.value()) {
setupScrollHandling();
} else {
setupScrollHandlingWithFilter();
}
}
void FlexibleScrollHelper::setupScrollAnimation() {
constexpr auto kScrollStepTime = crl::time(260);
const auto clearScrollState = [=] {
_scrollAnimation.stop();
_scrollTopFrom = 0;
_scrollTopTo = 0;
_timeOffset = 0;
_lastScrollApplied = 0;
};
_scrollAnimation.init([=](crl::time now) {
const auto progress = float64(now
- _scrollAnimation.started()
- _timeOffset) / kScrollStepTime;
const auto eased = anim::easeOutQuint(1.0, progress);
const auto scrollCurrent = anim::interpolate(
_scrollTopFrom,
_scrollTopTo,
std::clamp(eased, 0., 1.));
scrollToY(scrollCurrent);
_lastScrollApplied = scrollCurrent;
if (progress >= 1) {
clearScrollState();
}
});
}
void FlexibleScrollHelper::setupScrollHandling() {
const auto heightDiff = [=] {
return _pinnedToTop->maximumHeight()
- _pinnedToTop->minimumHeight();
};
rpl::combine(
_pinnedToTop->heightValue(),
_inner->heightValue()
) | rpl::on_next([=](int, int h) {
_data.contentHeightValue.fire(h + heightDiff());
}, _pinnedToTop->lifetime());
const auto singleStep = _scroll->verticalScrollBar()->singleStep()
* QApplication::wheelScrollLines();
const auto step1 = (_pinnedToTop->maximumHeight()
< st::infoProfileTopBarHeightMax)
? (st::infoProfileTopBarStep2 + st::lineWidth)
: st::infoProfileTopBarStep1;
const auto step2 = st::infoProfileTopBarStep2;
// const auto stepDepreciation = singleStep
// - st::infoProfileTopBarActionButtonsHeight;
_scrollTopPrevious = _scroll->scrollTop();
_scroll->scrollTopValue(
) | rpl::on_next([=](int top) {
if (_applyingFakeScrollState) {
return;
}
const auto diff = top - _scrollTopPrevious;
if (std::abs(diff) == singleStep) {
const auto previousValue = top - diff;
const auto nextStep = (diff > 0)
? ((previousValue == 0)
? step1
: (previousValue == step1)
? step2
: -1)
// : ((top < step1
// && (top + stepDepreciation != step1
// || _scrollAnimation.animating()))
: ((top < step1)
? 0
: (top < step2)
? step1
: -1);
{
_applyingFakeScrollState = true;
scrollToY(previousValue);
_applyingFakeScrollState = false;
}
if (_scrollAnimation.animating()
&& ((_scrollTopTo > _scrollTopFrom) != (diff > 0))) {
auto overriddenDirection = true;
if (_scrollTopTo > _scrollTopFrom) {
// From going down to going up.
if (_scrollTopTo == step1) {
_scrollTopTo = 0;
} else if (_scrollTopTo == step2) {
_scrollTopTo = step1;
} else {
overriddenDirection = false;
}
} else {
// From going up to going down.
if (_scrollTopTo == 0) {
_scrollTopTo = step1;
} else if (_scrollTopTo == step1) {
_scrollTopTo = step2;
} else {
overriddenDirection = false;
}
}
if (overriddenDirection) {
_timeOffset = crl::now() - _scrollAnimation.started();
_scrollTopFrom = _lastScrollApplied
? _lastScrollApplied
: previousValue;
return;
} else {
_scrollAnimation.stop();
_scrollTopFrom = 0;
_scrollTopTo = 0;
_timeOffset = 0;
_lastScrollApplied = 0;
}
}
_scrollTopFrom = _lastScrollApplied
? _lastScrollApplied
: previousValue;
if (!_scrollAnimation.animating()) {
_scrollTopTo = ((nextStep != -1) ? nextStep : top);
_scrollAnimation.start();
} else {
if (_scrollTopTo > _scrollTopFrom) {
// Down.
if (_scrollTopTo == step1) {
_scrollTopTo = step2;
} else {
_scrollTopTo += diff;
}
} else {
// Up.
if (_scrollTopTo == step2) {
_scrollTopTo = step1;
} else if (_scrollTopTo == step1) {
_scrollTopTo = 0;
} else {
_scrollTopTo += diff;
}
}
_timeOffset = (crl::now() - _scrollAnimation.started());
}
return;
}
_scrollTopPrevious = top;
const auto current = heightDiff() - top;
_inner->moveToLeft(0, std::min(0, current));
_pinnedToTop->resize(
_pinnedToTop->width(),
std::max(current + _pinnedToTop->minimumHeight(), 0));
}, _inner->lifetime());
_data.fillerWidthValue.events(
) | rpl::on_next([=](int w) {
_inner->resizeToWidth(w);
}, _inner->lifetime());
_setPaintPadding({ 0, _pinnedToTop->minimumHeight(), 0, 0 });
_setViewport(_pinnedToTop->events(
) | rpl::filter([](not_null<QEvent*> e) {
return e->type() == QEvent::Wheel;
}));
}
void FlexibleScrollHelper::setupScrollHandlingWithFilter() {
rpl::combine(
_pinnedToTop->heightValue(),
_inner->heightValue()
) | rpl::on_next([=](int, int h) {
const auto max = _pinnedToTop->maximumHeight();
const auto min = _pinnedToTop->minimumHeight();
const auto diff = max - min;
const auto progress = (diff > 0)
? std::clamp(
(_pinnedToTop->height() - min) / float64(diff),
0.,
1.)
: 1.;
_data.contentHeightValue.fire(h
+ anim::interpolate(diff, 0, progress));
}, _pinnedToTop->lifetime());
const auto singleStep = _scroll->verticalScrollBar()->singleStep()
* QApplication::wheelScrollLines();
const auto step1 = (_pinnedToTop->maximumHeight()
< st::infoProfileTopBarHeightMax)
? (st::infoProfileTopBarStep2 + st::lineWidth)
: st::infoProfileTopBarStep1;
const auto step2 = st::infoProfileTopBarStep2;
base::install_event_filter(_scroll->verticalScrollBar(), [=](
not_null<QEvent*> e) {
if (e->type() != QEvent::Wheel) {
return base::EventFilterResult::Continue;
}
const auto wheel = static_cast<QWheelEvent*>(e.get());
const auto delta = wheel->angleDelta().y();
if (std::abs(delta) != 120) {
scrollToY(_scroll->scrollTop() - delta);
return base::EventFilterResult::Cancel;
}
const auto actualTop = _scroll->scrollTop();
const auto animationActive = _scrollAnimation.animating()
&& (_lastScrollApplied != _scrollTopTo);
const auto top = animationActive
? (_lastScrollApplied ? _lastScrollApplied : actualTop)
: actualTop;
const auto diff = (delta > 0) ? -singleStep : singleStep;
const auto previousValue = top;
const auto targetTop = top + diff;
const auto nextStep = (diff > 0)
? ((previousValue == 0)
? step1
: (previousValue == step1)
? step2
: -1)
: ((targetTop < step1)
? 0
: (targetTop < step2)
? step1
: -1);
if (animationActive
&& ((_scrollTopTo > _scrollTopFrom) != (diff > 0))) {
auto overriddenDirection = true;
if (_scrollTopTo > _scrollTopFrom) {
if (_scrollTopTo == step1) {
_scrollTopTo = 0;
} else if (_scrollTopTo == step2) {
_scrollTopTo = step1;
} else {
overriddenDirection = false;
}
} else {
if (_scrollTopTo == 0) {
_scrollTopTo = step1;
} else if (_scrollTopTo == step1) {
_scrollTopTo = step2;
} else {
overriddenDirection = false;
}
}
if (overriddenDirection) {
_timeOffset = crl::now() - _scrollAnimation.started();
_scrollTopFrom = _lastScrollApplied
? _lastScrollApplied
: top;
return base::EventFilterResult::Cancel;
} else {
_scrollAnimation.stop();
_scrollTopFrom = 0;
_scrollTopTo = 0;
_timeOffset = 0;
_lastScrollApplied = 0;
}
}
_scrollTopFrom = top;
if (!animationActive) {
_scrollTopTo = (nextStep != -1) ? nextStep : targetTop;
_scrollAnimation.start();
} else {
if (_scrollTopTo > _scrollTopFrom) {
if (_scrollTopTo == step1) {
_scrollTopTo = step2;
} else {
_scrollTopTo += diff;
}
} else {
if (_scrollTopTo == step2) {
_scrollTopTo = step1;
} else if (_scrollTopTo == step1) {
_scrollTopTo = 0;
} else {
_scrollTopTo += diff;
}
}
_timeOffset = crl::now() - _scrollAnimation.started();
}
return base::EventFilterResult::Cancel;
}, _filterLifetime);
_scroll->scrollTopValue() | rpl::on_next([=](int top) {
applyScrollToPinnedLayout(top);
}, _inner->lifetime());
_data.fillerWidthValue.events(
) | rpl::on_next([=](int w) {
_inner->resizeToWidth(w);
}, _inner->lifetime());
_setPaintPadding({ 0, _pinnedToTop->minimumHeight(), 0, 0 });
_setViewport(_pinnedToTop->events(
) | rpl::filter([](not_null<QEvent*> e) {
return e->type() == QEvent::Wheel;
}));
}
void FlexibleScrollHelper::scrollToY(int scrollCurrent) {
applyScrollToPinnedLayout(scrollCurrent);
_scroll->scrollToY(scrollCurrent);
}
void FlexibleScrollHelper::applyScrollToPinnedLayout(int scrollCurrent) {
const auto top = std::min(scrollCurrent, _scroll->scrollTopMax());
const auto minimumHeight = _pinnedToTop->minimumHeight();
const auto current = _pinnedToTop->maximumHeight()
- minimumHeight
- top;
_inner->moveToLeft(0, std::min(0, current));
_pinnedToTop->resize(
_pinnedToTop->width(),
std::max(current + minimumHeight, 0));
}
} // namespace Info

View File

@@ -0,0 +1,58 @@
/*
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/widgets/scroll_area.h"
#include "ui/effects/animations.h"
#include "ui/rp_widget.h"
namespace Info {
extern const char kAlternativeScrollProcessing[];
struct FlexibleScrollData {
rpl::event_stream<int> contentHeightValue;
rpl::event_stream<int> fillerWidthValue;
rpl::event_stream<> backButtonEnables;
};
class FlexibleScrollHelper final {
public:
FlexibleScrollHelper(
not_null<Ui::ScrollArea*> scroll,
not_null<Ui::RpWidget*> inner,
not_null<Ui::RpWidget*> pinnedToTop,
Fn<void(QMargins)> setPaintPadding,
Fn<void(rpl::producer<not_null<QEvent*>>&&)> setViewport,
FlexibleScrollData &data);
private:
void setupScrollAnimation();
void setupScrollHandling();
void setupScrollHandlingWithFilter();
void scrollToY(int value);
void applyScrollToPinnedLayout(int scrollCurrent);
const not_null<Ui::ScrollArea*> _scroll;
const not_null<Ui::RpWidget*> _inner;
const not_null<Ui::RpWidget*> _pinnedToTop;
const Fn<void(QMargins)> _setPaintPadding;
const Fn<void(rpl::producer<not_null<QEvent*>>&&)> _setViewport;
FlexibleScrollData &_data;
Ui::Animations::Basic _scrollAnimation;
int _scrollTopFrom = 0;
int _scrollTopTo = 0;
crl::time _timeOffset = 0;
int _lastScrollApplied = 0;
int _scrollTopPrevious = 0;
bool _applyingFakeScrollState = false;
rpl::lifetime _filterLifetime;
};
} // namespace Info

View File

@@ -0,0 +1,387 @@
/*
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 "info/info_layer_widget.h"
#include "info/info_content_widget.h"
#include "info/info_top_bar.h"
#include "info/info_memento.h"
#include "ui/rp_widget.h"
#include "ui/focus_persister.h"
#include "ui/widgets/buttons.h"
#include "ui/cached_round_corners.h"
#include "window/section_widget.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "window/main_window.h"
#include "main/main_session.h"
#include "core/application.h"
#include "styles/style_info.h"
#include "styles/style_window.h"
#include "styles/style_layers.h"
namespace Info {
LayerWidget::LayerWidget(
not_null<Window::SessionController*> controller,
not_null<Memento*> memento)
: _controller(controller)
, _contentWrap(this, controller, Wrap::Layer, memento) {
setupHeightConsumers();
controller->window().replaceFloatPlayerDelegate(floatPlayerDelegate());
}
LayerWidget::LayerWidget(
not_null<Window::SessionController*> controller,
not_null<MoveMemento*> memento)
: _controller(controller)
, _contentWrap(memento->takeContent(this, Wrap::Layer)) {
setupHeightConsumers();
controller->window().replaceFloatPlayerDelegate(floatPlayerDelegate());
}
auto LayerWidget::floatPlayerDelegate()
-> not_null<::Media::Player::FloatDelegate*> {
return static_cast<::Media::Player::FloatDelegate*>(this);
}
not_null<Ui::RpWidget*> LayerWidget::floatPlayerWidget() {
return this;
}
void LayerWidget::floatPlayerToggleGifsPaused(bool paused) {
constexpr auto kReason = Window::GifPauseReason::RoundPlaying;
if (paused) {
_controller->enableGifPauseReason(kReason);
} else {
_controller->disableGifPauseReason(kReason);
}
}
auto LayerWidget::floatPlayerGetSection(Window::Column column)
-> not_null<::Media::Player::FloatSectionDelegate*> {
Expects(_contentWrap != nullptr);
return _contentWrap;
}
void LayerWidget::floatPlayerEnumerateSections(Fn<void(
not_null<::Media::Player::FloatSectionDelegate*> widget,
Window::Column widgetColumn)> callback) {
Expects(_contentWrap != nullptr);
callback(_contentWrap, Window::Column::Second);
}
bool LayerWidget::floatPlayerIsVisible(not_null<HistoryItem*> item) {
return false;
}
void LayerWidget::floatPlayerDoubleClickEvent(
not_null<const HistoryItem*> item) {
_controller->showMessage(item);
}
void LayerWidget::setupHeightConsumers() {
Expects(_contentWrap != nullptr);
_contentWrap->scrollTillBottomChanges(
) | rpl::filter([this] {
if (!_inResize) {
return true;
}
_pendingResize = true;
return false;
}) | rpl::on_next([this] {
resizeToWidth(width());
}, lifetime());
_contentWrap->grabbingForExpanding(
) | rpl::on_next([=](bool grabbing) {
if (grabbing) {
_savedHeight = _contentWrapHeight;
_savedHeightAnimation = base::take(_heightAnimation);
setContentHeight(_desiredHeight);
} else {
_heightAnimation = base::take(_savedHeightAnimation);
setContentHeight(_savedHeight);
}
}, lifetime());
_contentWrap->desiredHeightValue(
) | rpl::on_next([this](int height) {
if (!height) {
// New content arrived.
_heightAnimated = _heightAnimation.animating();
return;
}
std::swap(_desiredHeight, height);
if (!height
|| (_heightAnimated && !_heightAnimation.animating())) {
_heightAnimated = true;
setContentHeight(_desiredHeight);
} else {
_heightAnimated = true;
_heightAnimation.start([=] {
setContentHeight(_heightAnimation.value(_desiredHeight));
}, _contentWrapHeight, _desiredHeight, st::slideDuration);
resizeToWidth(width());
}
}, lifetime());
}
void LayerWidget::setContentHeight(int height) {
if (_contentWrapHeight == height) {
return;
}
_contentWrapHeight = height;
if (_inResize) {
_pendingResize = true;
} else if (_contentWrap) {
resizeToWidth(width());
}
}
void LayerWidget::showFinished() {
floatPlayerShowVisible();
_contentWrap->showFast();
}
void LayerWidget::parentResized() {
if (!_contentWrap) {
return;
}
auto parentSize = parentWidget()->size();
auto parentWidth = parentSize.width();
if (parentWidth < MinimalSupportedWidth()) {
Ui::FocusPersister persister(this);
restoreFloatPlayerDelegate();
auto memento = std::make_shared<MoveMemento>(std::move(_contentWrap));
// We want to call hideSpecialLayer synchronously to avoid glitches,
// but we can't destroy LayerStackWidget from its' resizeEvent,
// because QWidget has such code for resizing:
//
// QResizeEvent e(r.size(), olds);
// QApplication::sendEvent(q, &e);
// if (q->windowHandle())
// q->update();
//
// So we call it queued. It would be cool to call it 'right after'
// the resize event handling was finished.
InvokeQueued(this, [=] {
_controller->hideSpecialLayer(anim::type::instant);
});
_controller->showSection(
std::move(memento),
Window::SectionShow(
Window::SectionShow::Way::Forward,
anim::type::instant,
anim::activation::background));
//
// There was a layout logic which caused layer info to become a
// third column info if the window size allows, but it was decided
// to keep layer info and third column info separated.
//
//} else if (_controller->canShowThirdSectionWithoutResize()) {
// takeToThirdSection();
} else {
auto newWidth = qMin(
parentWidth - 2 * st::infoMinimalLayerMargin,
st::infoDesiredWidth);
resizeToWidth(newWidth);
}
}
bool LayerWidget::takeToThirdSection() {
return false;
//
// There was a layout logic which caused layer info to become a
// third column info if the window size allows, but it was decided
// to keep layer info and third column info separated.
//
//Ui::FocusPersister persister(this);
//auto localCopy = _controller;
//auto memento = MoveMemento(std::move(_contentWrap));
//localCopy->hideSpecialLayer(anim::type::instant);
//// When creating third section in response to the window
//// size allowing it to fit without window resize we want
//// to save that we didn't extend the window while showing
//// the third section, so that when we close it we won't
//// shrink the window size.
////
//// See https://github.com/telegramdesktop/tdesktop/issues/4091
//localCopy->session()().settings().setThirdSectionExtendedBy(0);
//localCopy->session()().settings().setThirdSectionInfoEnabled(true);
//localCopy->session()().saveSettingsDelayed();
//localCopy->showSection(
// std::move(memento),
// Window::SectionShow(
// Window::SectionShow::Way::ClearStack,
// anim::type::instant,
// anim::activation::background));
//return true;
}
bool LayerWidget::showSectionInternal(
not_null<Window::SectionMemento*> memento,
const Window::SectionShow &params) {
if (_contentWrap && _contentWrap->showInternal(memento, params)) {
if (params.activation != anim::activation::background) {
_controller->parentController()->hideLayer();
}
return true;
}
return false;
}
bool LayerWidget::closeByOutsideClick() const {
return _contentWrap ? _contentWrap->closeByOutsideClick() : true;
}
int LayerWidget::MinimalSupportedWidth() {
const auto minimalMargins = 2 * st::infoMinimalLayerMargin;
return st::infoMinimalWidth + minimalMargins;
}
int LayerWidget::resizeGetHeight(int newWidth) {
if (!parentWidget() || !_contentWrap || !newWidth) {
return 0;
}
constexpr auto kMaxAttempts = 16;
auto attempts = 0;
while (true) {
_inResize = true;
const auto newGeometry = countGeometry(newWidth);
_inResize = false;
if (!_pendingResize) {
const auto oldGeometry = geometry();
if (newGeometry != oldGeometry) {
_contentWrap->forceContentRepaint();
}
if (newGeometry.topLeft() != oldGeometry.topLeft()) {
move(newGeometry.topLeft());
}
floatPlayerUpdatePositions();
return newGeometry.height();
}
_pendingResize = false;
Assert(attempts++ < kMaxAttempts);
}
}
QRect LayerWidget::countGeometry(int newWidth) {
const auto &parentSize = parentWidget()->size();
const auto windowWidth = parentSize.width();
const auto windowHeight = parentSize.height();
const auto newLeft = (windowWidth - newWidth) / 2;
const auto newTop = std::clamp(
windowHeight / 24,
st::infoLayerTopMinimal,
st::infoLayerTopMaximal);
const auto newBottom = newTop;
const auto bottomRadius = st::boxRadius;
const auto maxVisibleHeight = windowHeight - newTop;
// Top rounding is included in _contentWrapHeight.
auto desiredHeight = _contentWrapHeight + bottomRadius;
accumulate_min(desiredHeight, maxVisibleHeight - newBottom);
// First resize content to new width and get the new desired height.
const auto contentLeft = 0;
const auto contentTop = 0;
const auto contentBottom = bottomRadius;
const auto contentWidth = newWidth;
auto contentHeight = desiredHeight - contentTop - contentBottom;
const auto scrollTillBottom = _contentWrap->scrollTillBottom(
contentHeight);
auto additionalScroll = std::min(scrollTillBottom, newBottom);
const auto expanding = (_desiredHeight > _contentWrapHeight);
desiredHeight += additionalScroll;
contentHeight += additionalScroll;
_tillBottom = (desiredHeight >= maxVisibleHeight);
if (_tillBottom) {
additionalScroll += contentBottom;
}
_contentTillBottom = _tillBottom && !_contentWrap->scrollBottomSkip();
if (_contentTillBottom) {
contentHeight += contentBottom;
}
_contentWrap->updateGeometry({
contentLeft,
contentTop,
contentWidth,
contentHeight,
}, expanding, additionalScroll, maxVisibleHeight);
return QRect(newLeft, newTop, newWidth, desiredHeight);
}
void LayerWidget::doSetInnerFocus() {
if (_contentWrap) {
_contentWrap->setInnerFocus();
}
}
void LayerWidget::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
const auto clip = e->rect();
const auto radius = st::boxRadius;
const auto &corners = Ui::CachedCornerPixmaps(Ui::BoxCorners);
if (!_tillBottom) {
const auto bottom = QRect{ 0, height() - radius, width(), radius };
if (clip.intersects(bottom)) {
if (const auto rounding = _contentWrap->bottomSkipRounding()) {
rounding->paint(p, rect(), RectPart::FullBottom);
} else {
Ui::FillRoundRect(p, bottom, st::boxBg, {
.p = { QPixmap(), QPixmap(), corners.p[2], corners.p[3] }
});
}
}
} else if (!_contentTillBottom) {
const auto rounding = _contentWrap->bottomSkipRounding();
const auto &color = rounding ? rounding->color() : st::boxBg;
p.fillRect(0, height() - radius, width(), radius, color);
}
if (_contentWrap->animatingShow()) {
const auto top = QRect{ 0, 0, width(), radius };
if (clip.intersects(top)) {
Ui::FillRoundRect(p, top, st::boxBg, {
.p = { corners.p[0], corners.p[1], QPixmap(), QPixmap() }
});
}
p.fillRect(0, radius, width(), height() - 2 * radius, st::boxBg);
}
}
void LayerWidget::restoreFloatPlayerDelegate() {
if (!_floatPlayerDelegateRestored) {
_floatPlayerDelegateRestored = true;
_controller->window().restoreFloatPlayerDelegate(
floatPlayerDelegate());
}
}
void LayerWidget::closeHook() {
restoreFloatPlayerDelegate();
}
LayerWidget::~LayerWidget() {
if (!Core::Quitting()) {
restoreFloatPlayerDelegate();
}
}
} // namespace Info

View File

@@ -0,0 +1,93 @@
/*
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/layers/layer_widget.h"
#include "media/player/media_player_float.h"
namespace Window {
class SessionController;
} // namespace Window
namespace Info {
class Memento;
class MoveMemento;
class WrapWidget;
class TopBar;
class LayerWidget
: public Ui::LayerWidget
, private ::Media::Player::FloatDelegate {
public:
LayerWidget(
not_null<Window::SessionController*> controller,
not_null<Memento*> memento);
LayerWidget(
not_null<Window::SessionController*> controller,
not_null<MoveMemento*> memento);
void showFinished() override;
void parentResized() override;
bool takeToThirdSection() override;
bool showSectionInternal(
not_null<Window::SectionMemento*> memento,
const Window::SectionShow &params) override;
bool closeByOutsideClick() const override;
static int MinimalSupportedWidth();
~LayerWidget();
protected:
int resizeGetHeight(int newWidth) override;
void doSetInnerFocus() override;
void paintEvent(QPaintEvent *e) override;
private:
void closeHook() override;
void restoreFloatPlayerDelegate();
not_null<::Media::Player::FloatDelegate*> floatPlayerDelegate();
not_null<Ui::RpWidget*> floatPlayerWidget() override;
void floatPlayerToggleGifsPaused(bool paused) override;
not_null<::Media::Player::FloatSectionDelegate*> floatPlayerGetSection(
Window::Column column) override;
void floatPlayerEnumerateSections(Fn<void(
not_null<::Media::Player::FloatSectionDelegate*> widget,
Window::Column widgetColumn)> callback) override;
bool floatPlayerIsVisible(not_null<HistoryItem*> item) override;
void floatPlayerDoubleClickEvent(
not_null<const HistoryItem*> item) override;
void setupHeightConsumers();
void setContentHeight(int height);
[[nodiscard]] QRect countGeometry(int newWidth);
not_null<Window::SessionController*> _controller;
object_ptr<WrapWidget> _contentWrap;
int _desiredHeight = 0;
int _contentWrapHeight = 0;
int _savedHeight = 0;
Ui::Animations::Simple _heightAnimation;
Ui::Animations::Simple _savedHeightAnimation;
bool _heightAnimated = false;
bool _inResize = false;
bool _pendingResize = false;
bool _tillBottom = false;
bool _contentTillBottom = false;
bool _floatPlayerDelegateRestored = false;
};
} // namespace Info

View File

@@ -0,0 +1,327 @@
/*
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 "info/info_memento.h"
#include "info/global_media/info_global_media_widget.h"
#include "info/profile/info_profile_widget.h"
#include "info/media/info_media_widget.h"
#include "info/members/info_members_widget.h"
#include "info/common_groups/info_common_groups_widget.h"
#include "info/saved/info_saved_sublists_widget.h"
#include "info/settings/info_settings_widget.h"
#include "info/similar_peers/info_similar_peers_widget.h"
#include "info/reactions_list/info_reactions_list_widget.h"
#include "info/requests_list/info_requests_list_widget.h"
#include "info/peer_gifts/info_peer_gifts_widget.h"
#include "info/polls/info_polls_results_widget.h"
#include "info/info_section_widget.h"
#include "info/info_layer_widget.h"
#include "info/info_controller.h"
#include "ui/ui_utility.h"
#include "boxes/peer_list_box.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_forum_topic.h"
#include "data/data_saved_sublist.h"
#include "data/data_session.h"
#include "main/main_session.h"
namespace Info {
Memento::Memento(not_null<PeerData*> peer)
: Memento(peer, Section::Type::Profile) {
}
Memento::Memento(not_null<PeerData*> peer, Section section)
: Memento(DefaultStack(peer, section)) {
}
Memento::Memento(not_null<Data::ForumTopic*> topic)
: Memento(topic, Section::Type::Profile) {
}
Memento::Memento(not_null<Data::ForumTopic*> topic, Section section)
: Memento(DefaultStack(topic, section)) {
}
Memento::Memento(not_null<Data::SavedSublist*> sublist)
: Memento(sublist, Section::Type::Profile) {
}
Memento::Memento(not_null<Data::SavedSublist*> sublist, Section section)
: Memento(DefaultStack(sublist, section)) {
}
Memento::Memento(Settings::Tag settings, Section section)
: Memento(DefaultStack(settings, section)) {
}
Memento::Memento(not_null<PollData*> poll, FullMsgId contextId)
: Memento(DefaultStack(poll, contextId)) {
}
Memento::Memento(
std::shared_ptr<Api::WhoReadList> whoReadIds,
FullMsgId contextId,
Data::ReactionId selected)
: Memento(DefaultStack(std::move(whoReadIds), contextId, selected)) {
}
Memento::Memento(std::vector<std::shared_ptr<ContentMemento>> stack)
: _stack(std::move(stack)) {
auto topics = base::flat_set<not_null<Data::ForumTopic*>>();
auto sublists = base::flat_set<not_null<Data::SavedSublist*>>();
for (auto &entry : _stack) {
if (const auto topic = entry->topic()) {
topics.emplace(topic);
} else if (const auto sublist = entry->sublist()) {
sublists.emplace(sublist);
}
}
for (const auto &topic : topics) {
topic->destroyed(
) | rpl::on_next([=] {
for (auto i = begin(_stack); i != end(_stack);) {
if (i->get()->topic() == topic) {
i = _stack.erase(i);
} else {
++i;
}
}
if (_stack.empty()) {
_removeRequests.fire({});
}
}, _lifetime);
}
for (const auto &sublist : sublists) {
sublist->destroyed(
) | rpl::on_next([=] {
for (auto i = begin(_stack); i != end(_stack);) {
if (i->get()->sublist() == sublist) {
i = _stack.erase(i);
} else {
++i;
}
}
if (_stack.empty()) {
_removeRequests.fire({});
}
}, _lifetime);
}
}
std::vector<std::shared_ptr<ContentMemento>> Memento::DefaultStack(
not_null<PeerData*> peer,
Section section) {
auto result = std::vector<std::shared_ptr<ContentMemento>>();
result.push_back(DefaultContent(peer, section));
return result;
}
std::vector<std::shared_ptr<ContentMemento>> Memento::DefaultStack(
not_null<Data::ForumTopic*> topic,
Section section) {
auto result = std::vector<std::shared_ptr<ContentMemento>>();
result.push_back(DefaultContent(topic, section));
return result;
}
std::vector<std::shared_ptr<ContentMemento>> Memento::DefaultStack(
not_null<Data::SavedSublist*> sublist,
Section section) {
auto result = std::vector<std::shared_ptr<ContentMemento>>();
result.push_back(DefaultContent(sublist, section));
return result;
}
std::vector<std::shared_ptr<ContentMemento>> Memento::DefaultStack(
Settings::Tag settings,
Section section) {
auto result = std::vector<std::shared_ptr<ContentMemento>>();
result.push_back(std::make_shared<Settings::Memento>(
settings.self,
section.settingsType()));
return result;
}
std::vector<std::shared_ptr<ContentMemento>> Memento::DefaultStack(
not_null<PollData*> poll,
FullMsgId contextId) {
auto result = std::vector<std::shared_ptr<ContentMemento>>();
result.push_back(std::make_shared<Polls::Memento>(poll, contextId));
return result;
}
std::vector<std::shared_ptr<ContentMemento>> Memento::DefaultStack(
std::shared_ptr<Api::WhoReadList> whoReadIds,
FullMsgId contextId,
Data::ReactionId selected) {
auto result = std::vector<std::shared_ptr<ContentMemento>>();
result.push_back(std::make_shared<ReactionsList::Memento>(
std::move(whoReadIds),
contextId,
selected));
return result;
}
Section Memento::DefaultSection(not_null<PeerData*> peer) {
if (peer->savedSublistsInfo()) {
return Section(Section::Type::SavedSublists);
} else if (peer->sharedMediaInfo()) {
return Section(Section::MediaType::Photo);
}
return Section(Section::Type::Profile);
}
std::shared_ptr<Memento> Memento::Default(not_null<PeerData*> peer) {
return std::make_shared<Memento>(peer, DefaultSection(peer));
}
std::shared_ptr<ContentMemento> Memento::DefaultContent(
not_null<PeerData*> peer,
Section section) {
if (auto to = peer->migrateTo()) {
peer = to;
}
auto migrated = peer->migrateFrom();
auto migratedPeerId = migrated ? migrated->id : PeerId(0);
switch (section.type()) {
case Section::Type::Profile:
return std::make_shared<Profile::Memento>(
peer,
migratedPeerId);
case Section::Type::Media:
return std::make_shared<Media::Memento>(
peer,
migratedPeerId,
section.mediaType());
case Section::Type::GlobalMedia:
return std::make_shared<GlobalMedia::Memento>(
peer->asUser(),
section.mediaType());
case Section::Type::CommonGroups:
return std::make_shared<CommonGroups::Memento>(peer->asUser());
case Section::Type::SimilarPeers:
return std::make_shared<SimilarPeers::Memento>(peer);
case Section::Type::RequestsList:
return std::make_shared<RequestsList::Memento>(peer);
case Section::Type::SavedSublists:
return std::make_shared<Saved::SublistsMemento>(&peer->session());
case Section::Type::Members:
return std::make_shared<Members::Memento>(
peer,
migratedPeerId);
}
Unexpected("Wrong section type in Info::Memento::DefaultContent()");
}
std::shared_ptr<ContentMemento> Memento::DefaultContent(
not_null<Data::ForumTopic*> topic,
Section section) {
const auto peer = topic->peer();
const auto migrated = peer->migrateFrom();
const auto migratedPeerId = migrated ? migrated->id : PeerId(0);
switch (section.type()) {
case Section::Type::Profile:
return std::make_shared<Profile::Memento>(topic);
case Section::Type::Media:
return std::make_shared<Media::Memento>(topic, section.mediaType());
case Section::Type::Members:
return std::make_shared<Members::Memento>(peer, migratedPeerId);
}
Unexpected("Wrong section type in Info::Memento::DefaultContent()");
}
std::shared_ptr<ContentMemento> Memento::DefaultContent(
not_null<Data::SavedSublist*> sublist,
Section section) {
switch (section.type()) {
case Section::Type::Profile:
return std::make_shared<Profile::Memento>(sublist);
case Section::Type::Media:
return std::make_shared<Media::Memento>(
sublist,
section.mediaType());
}
Unexpected("Wrong section type in Info::Memento::DefaultContent()");
}
object_ptr<Window::SectionWidget> Memento::createWidget(
QWidget *parent,
not_null<Window::SessionController*> controller,
Window::Column column,
const QRect &geometry) {
auto wrap = (column == Window::Column::Third)
? Wrap::Side
: Wrap::Narrow;
auto result = object_ptr<SectionWidget>(
parent,
controller,
wrap,
this);
result->setGeometry(geometry);
return result;
}
object_ptr<Ui::LayerWidget> Memento::createLayer(
not_null<Window::SessionController*> controller,
const QRect &geometry) {
if (geometry.width() >= LayerWidget::MinimalSupportedWidth()) {
return object_ptr<LayerWidget>(controller, this);
}
return nullptr;
}
std::vector<std::shared_ptr<ContentMemento>> Memento::takeStack() {
return std::move(_stack);
}
Memento::~Memento() = default;
MoveMemento::MoveMemento(object_ptr<WrapWidget> content)
: _content(std::move(content)) {
_content->hide();
_content->setParent(nullptr);
}
object_ptr<Window::SectionWidget> MoveMemento::createWidget(
QWidget *parent,
not_null<Window::SessionController*> controller,
Window::Column column,
const QRect &geometry) {
auto wrap = (column == Window::Column::Third)
? Wrap::Side
: Wrap::Narrow;
auto result = object_ptr<SectionWidget>(
parent,
controller,
wrap,
this);
result->setGeometry(geometry);
return result;
}
object_ptr<Ui::LayerWidget> MoveMemento::createLayer(
not_null<Window::SessionController*> controller,
const QRect &geometry) {
if (geometry.width() < LayerWidget::MinimalSupportedWidth()) {
return nullptr;
}
return object_ptr<LayerWidget>(controller, this);
}
object_ptr<WrapWidget> MoveMemento::takeContent(
QWidget *parent,
Wrap wrap) {
Ui::AttachParentChild(parent, _content);
_content->setWrap(wrap);
return std::move(_content);
}
} // namespace Info

View File

@@ -0,0 +1,157 @@
/*
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/rp_widget.h"
#include "info/info_wrap_widget.h"
#include "dialogs/dialogs_key.h"
#include "window/section_memento.h"
#include "base/object_ptr.h"
namespace Api {
struct WhoReadList;
} // namespace Api
namespace Storage {
enum class SharedMediaType : signed char;
} // namespace Storage
namespace Data {
class ForumTopic;
class SavedSublist;
struct ReactionId;
} // namespace Data
namespace Ui {
class ScrollArea;
struct ScrollToRequest;
} // namespace Ui
namespace Info {
namespace Settings {
struct Tag;
} // namespace Settings
namespace Downloads {
struct Tag;
} // namespace Downloads
class ContentMemento;
class WrapWidget;
class Memento final : public Window::SectionMemento {
public:
explicit Memento(not_null<PeerData*> peer);
Memento(not_null<PeerData*> peer, Section section);
explicit Memento(not_null<Data::ForumTopic*> topic);
Memento(not_null<Data::ForumTopic*> topic, Section section);
explicit Memento(not_null<Data::SavedSublist*> sublist);
Memento(not_null<Data::SavedSublist*> sublist, Section section);
Memento(Settings::Tag settings, Section section);
Memento(not_null<PollData*> poll, FullMsgId contextId);
Memento(
std::shared_ptr<Api::WhoReadList> whoReadIds,
FullMsgId contextId,
Data::ReactionId selected);
explicit Memento(std::vector<std::shared_ptr<ContentMemento>> stack);
object_ptr<Window::SectionWidget> createWidget(
QWidget *parent,
not_null<Window::SessionController*> controller,
Window::Column column,
const QRect &geometry) override;
object_ptr<Ui::LayerWidget> createLayer(
not_null<Window::SessionController*> controller,
const QRect &geometry) override;
rpl::producer<> removeRequests() const override {
return _removeRequests.events();
}
int stackSize() const {
return int(_stack.size());
}
std::vector<std::shared_ptr<ContentMemento>> takeStack();
not_null<ContentMemento*> content() {
Expects(!_stack.empty());
return _stack.back().get();
}
static Section DefaultSection(not_null<PeerData*> peer);
static std::shared_ptr<Memento> Default(not_null<PeerData*> peer);
~Memento();
private:
static std::vector<std::shared_ptr<ContentMemento>> DefaultStack(
not_null<PeerData*> peer,
Section section);
static std::vector<std::shared_ptr<ContentMemento>> DefaultStack(
not_null<Data::ForumTopic*> topic,
Section section);
static std::vector<std::shared_ptr<ContentMemento>> DefaultStack(
not_null<Data::SavedSublist*> sublist,
Section section);
static std::vector<std::shared_ptr<ContentMemento>> DefaultStack(
Settings::Tag settings,
Section section);
static std::vector<std::shared_ptr<ContentMemento>> DefaultStack(
not_null<PollData*> poll,
FullMsgId contextId);
static std::vector<std::shared_ptr<ContentMemento>> DefaultStack(
std::shared_ptr<Api::WhoReadList> whoReadIds,
FullMsgId contextId,
Data::ReactionId selected);
static std::shared_ptr<ContentMemento> DefaultContent(
not_null<PeerData*> peer,
Section section);
static std::shared_ptr<ContentMemento> DefaultContent(
not_null<Data::ForumTopic*> topic,
Section section);
static std::shared_ptr<ContentMemento> DefaultContent(
not_null<Data::SavedSublist*> sublist,
Section section);
std::vector<std::shared_ptr<ContentMemento>> _stack;
rpl::event_stream<> _removeRequests;
rpl::lifetime _lifetime;
};
class MoveMemento final : public Window::SectionMemento {
public:
MoveMemento(object_ptr<WrapWidget> content);
object_ptr<Window::SectionWidget> createWidget(
QWidget *parent,
not_null<Window::SessionController*> controller,
Window::Column column,
const QRect &geometry) override;
object_ptr<Ui::LayerWidget> createLayer(
not_null<Window::SessionController*> controller,
const QRect &geometry) override;
bool instant() const override {
return true;
}
object_ptr<WrapWidget> takeContent(
QWidget *parent,
Wrap wrap);
private:
object_ptr<WrapWidget> _content;
};
} // namespace Info

View File

@@ -0,0 +1,143 @@
/*
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 "info/info_section_widget.h"
#include "window/window_adaptive.h"
#include "window/window_connecting_widget.h"
#include "window/window_session_controller.h"
#include "main/main_session.h"
#include "info/info_content_widget.h"
#include "info/info_wrap_widget.h"
#include "info/info_layer_widget.h"
#include "info/info_memento.h"
#include "info/info_controller.h"
#include "styles/style_layers.h"
namespace Info {
SectionWidget::SectionWidget(
QWidget *parent,
not_null<Window::SessionController*> window,
Wrap wrap,
not_null<Memento*> memento)
: Window::SectionWidget(parent, window)
, _content(this, window, wrap, memento) {
init();
}
SectionWidget::SectionWidget(
QWidget *parent,
not_null<Window::SessionController*> window,
Wrap wrap,
not_null<MoveMemento*> memento)
: Window::SectionWidget(parent, window)
, _content(memento->takeContent(this, wrap)) {
init();
}
void SectionWidget::init() {
Expects(_connecting == nullptr);
rpl::combine(
sizeValue(),
_content->desiredHeightValue()
) | rpl::filter([=] {
return (_content != nullptr);
}) | rpl::on_next([=](QSize size, int) {
const auto expanding = false;
const auto full = !_content->scrollBottomSkip();
const auto additionalScroll = (full ? st::boxRadius : 0);
const auto height = size.height() - (full ? 0 : st::boxRadius);
const auto wrapGeometry = QRect{ 0, 0, size.width(), height };
_content->updateGeometry(
wrapGeometry,
expanding,
additionalScroll,
size.height());
}, lifetime());
_connecting = std::make_unique<Window::ConnectionState>(
_content.data(),
&controller()->session().account(),
controller()->adaptive().oneColumnValue());
_content->contentChanged(
) | rpl::on_next([=] {
_connecting->raise();
}, _connecting->lifetime());
}
Dialogs::RowDescriptor SectionWidget::activeChat() const {
return _content->activeChat();
}
bool SectionWidget::hasTopBarShadow() const {
return _content->hasTopBarShadow();
}
QPixmap SectionWidget::grabForShowAnimation(
const Window::SectionSlideParams &params) {
return _content->grabForShowAnimation(params);
}
void SectionWidget::doSetInnerFocus() {
_content->setInnerFocus();
}
void SectionWidget::showFinishedHook() {
_topBarSurrogate.destroy();
_content->showFast();
}
void SectionWidget::showAnimatedHook(
const Window::SectionSlideParams &params) {
_topBarSurrogate = _content->createTopBarSurrogate(this);
}
void SectionWidget::paintEvent(QPaintEvent *e) {
Window::SectionWidget::paintEvent(e);
if (!animatingShow()) {
QPainter(this).fillRect(e->rect(), st::windowBg);
}
}
bool SectionWidget::showInternal(
not_null<Window::SectionMemento*> memento,
const Window::SectionShow &params) {
return _content->showInternal(memento, params);
}
std::shared_ptr<Window::SectionMemento> SectionWidget::createMemento() {
return _content->createMemento();
}
object_ptr<Ui::LayerWidget> SectionWidget::moveContentToLayer(
QRect bodyGeometry) {
if (_content->controller()->wrap() != Wrap::Narrow
|| width() < LayerWidget::MinimalSupportedWidth()) {
return nullptr;
}
return MoveMemento(
std::move(_content)).createLayer(
controller(),
bodyGeometry);
}
rpl::producer<> SectionWidget::removeRequests() const {
return _content->removeRequests();
}
bool SectionWidget::floatPlayerHandleWheelEvent(QEvent *e) {
return _content->floatPlayerHandleWheelEvent(e);
}
QRect SectionWidget::floatPlayerAvailableRect() {
return _content->floatPlayerAvailableRect();
}
} // namespace Info

View File

@@ -0,0 +1,75 @@
/*
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 <rpl/event_stream.h>
#include "window/section_widget.h"
namespace Window {
class ConnectionState;
} // namespace Window
namespace Info {
class Memento;
class MoveMemento;
class Controller;
class WrapWidget;
enum class Wrap;
class SectionWidget final : public Window::SectionWidget {
public:
SectionWidget(
QWidget *parent,
not_null<Window::SessionController*> window,
Wrap wrap,
not_null<Memento*> memento);
SectionWidget(
QWidget *parent,
not_null<Window::SessionController*> window,
Wrap wrap,
not_null<MoveMemento*> memento);
Dialogs::RowDescriptor activeChat() const override;
bool hasTopBarShadow() const override;
QPixmap grabForShowAnimation(
const Window::SectionSlideParams &params) override;
bool showInternal(
not_null<Window::SectionMemento*> memento,
const Window::SectionShow &params) override;
std::shared_ptr<Window::SectionMemento> createMemento() override;
object_ptr<Ui::LayerWidget> moveContentToLayer(
QRect bodyGeometry) override;
rpl::producer<> removeRequests() const override;
// Float player interface.
bool floatPlayerHandleWheelEvent(QEvent *e) override;
QRect floatPlayerAvailableRect() override;
protected:
void doSetInnerFocus() override;
void showFinishedHook() override;
void showAnimatedHook(
const Window::SectionSlideParams &params) override;
void paintEvent(QPaintEvent *e) override;
private:
void init();
object_ptr<WrapWidget> _content;
object_ptr<Ui::RpWidget> _topBarSurrogate = { nullptr };
std::unique_ptr<Window::ConnectionState> _connecting;
};
} // namespace Info

View File

@@ -0,0 +1,816 @@
/*
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 "info/info_top_bar.h"
#include "dialogs/ui/dialogs_stories_list.h"
#include "lang/lang_keys.h"
#include "info/info_wrap_widget.h"
#include "info/info_controller.h"
#include "info/profile/info_profile_values.h"
#include "storage/storage_shared_media.h"
#include "boxes/delete_messages_box.h"
#include "boxes/peer_list_controllers.h"
#include "main/main_session.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/shadow.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/search_field_controller.h"
#include "data/data_session.h"
#include "data/data_channel.h"
#include "data/data_user.h"
#include "styles/style_dialogs.h"
#include "styles/style_info.h"
namespace Info {
TopBar::TopBar(
QWidget *parent,
not_null<Window::SessionNavigation*> navigation,
const style::InfoTopBar &st,
SelectedItems &&selectedItems)
: RpWidget(parent)
, _navigation(navigation)
, _st(st)
, _selectedItems(Section::MediaType::kCount) {
if (_st.radius) {
_roundRect.emplace(_st.radius, _st.bg);
}
setAttribute(Qt::WA_OpaquePaintEvent, !_roundRect);
setSelectedItems(std::move(selectedItems));
updateControlsVisibility(anim::type::instant);
}
template <typename Callback>
void TopBar::registerUpdateControlCallback(
QObject *guard,
Callback &&callback) {
_updateControlCallbacks[guard] =[
weak = base::make_weak(guard),
callback = std::forward<Callback>(callback)
](anim::type animated) {
if (!weak) {
return false;
}
callback(animated);
return true;
};
}
template <typename Widget, typename IsVisible>
void TopBar::registerToggleControlCallback(
Widget *widget,
IsVisible &&callback) {
registerUpdateControlCallback(widget, [
widget,
isVisible = std::forward<IsVisible>(callback)
](anim::type animated) {
widget->toggle(isVisible(), animated);
});
}
void TopBar::setTitle(TitleDescriptor descriptor) {
if (_title) {
delete _title;
}
if (_subtitle) {
delete _subtitle;
}
const auto withSubtitle = !!descriptor.subtitle;
if (withSubtitle) {
_subtitle = Ui::CreateChild<Ui::FadeWrap<Ui::FlatLabel>>(
this,
object_ptr<Ui::FlatLabel>(
this,
std::move(descriptor.subtitle),
_st.subtitle),
st::infoTopBarScale);
_subtitle->setDuration(st::infoTopBarDuration);
_subtitle->toggle(
!selectionMode() && !storiesTitle(),
anim::type::instant);
registerToggleControlCallback(_subtitle.data(), [=] {
return !selectionMode() && !storiesTitle() && !searchMode();
});
}
_title = Ui::CreateChild<Ui::FadeWrap<Ui::FlatLabel>>(
this,
object_ptr<Ui::FlatLabel>(
this,
std::move(descriptor.title),
withSubtitle ? _st.titleWithSubtitle : _st.title),
st::infoTopBarScale);
_title->setDuration(st::infoTopBarDuration);
_title->toggle(
!selectionMode() && !storiesTitle(),
anim::type::instant);
registerToggleControlCallback(_title.data(), [=] {
return !selectionMode() && !storiesTitle() && !searchMode();
});
if (_back) {
_title->setAttribute(Qt::WA_TransparentForMouseEvents);
if (_subtitle) {
_subtitle->setAttribute(Qt::WA_TransparentForMouseEvents);
}
}
updateControlsGeometry(width());
}
void TopBar::enableBackButton() {
if (_back) {
return;
}
_back = Ui::CreateChild<Ui::FadeWrap<Ui::IconButton>>(
this,
object_ptr<Ui::IconButton>(this, _st.back),
st::infoTopBarScale);
_back->setDuration(st::infoTopBarDuration);
_back->toggle(!selectionMode(), anim::type::instant);
_back->entity()->clicks(
) | rpl::to_empty
| rpl::start_to_stream(_backClicks, _back->lifetime());
registerToggleControlCallback(_back.data(), [=] {
return !selectionMode();
});
if (_title) {
_title->setAttribute(Qt::WA_TransparentForMouseEvents);
}
if (_subtitle) {
_subtitle->setAttribute(Qt::WA_TransparentForMouseEvents);
}
if (_storiesWrap) {
_storiesWrap->raise();
}
updateControlsGeometry(width());
}
void TopBar::createSearchView(
not_null<Ui::SearchFieldController*> controller,
rpl::producer<bool> &&shown,
bool startsFocused) {
setSearchField(
controller->createField(this, _st.searchRow.field),
std::move(shown),
startsFocused);
}
bool TopBar::focusSearchField() {
if (_searchField && _searchField->isVisible()) {
_searchField->setFocus();
return true;
}
return false;
}
Ui::FadeWrap<Ui::RpWidget> *TopBar::pushButton(
base::unique_qptr<Ui::RpWidget> button) {
auto wrapped = base::make_unique_q<Ui::FadeWrap<Ui::RpWidget>>(
this,
object_ptr<Ui::RpWidget>::fromRaw(button.release()),
st::infoTopBarScale);
auto weak = wrapped.get();
_buttons.push_back(std::move(wrapped));
weak->setDuration(st::infoTopBarDuration);
registerToggleControlCallback(weak, [=] {
return !selectionMode()
&& !_searchModeEnabled;
});
weak->toggle(
!selectionMode() && !_searchModeEnabled,
anim::type::instant);
weak->widthValue(
) | rpl::on_next([this] {
updateControlsGeometry(width());
}, lifetime());
return weak;
}
void TopBar::forceButtonVisibility(
Ui::FadeWrap<Ui::RpWidget> *button,
rpl::producer<bool> shown) {
_updateControlCallbacks.erase(button);
button->toggleOn(std::move(shown));
}
void TopBar::setSearchField(
base::unique_qptr<Ui::InputField> field,
rpl::producer<bool> &&shown,
bool startsFocused) {
Expects(field != nullptr);
createSearchView(field.release(), std::move(shown), startsFocused);
}
void TopBar::clearSearchField() {
_searchView = nullptr;
}
void TopBar::checkBeforeCloseByEscape(Fn<void()> close) {
if (_searchModeEnabled) {
if (_searchField && !_searchField->empty()) {
_searchField->setText({});
} else {
_searchModeEnabled = false;
updateControlsVisibility(anim::type::normal);
}
} else {
close();
}
}
void TopBar::createSearchView(
not_null<Ui::InputField*> field,
rpl::producer<bool> &&shown,
bool startsFocused) {
_searchView = base::make_unique_q<Ui::FixedHeightWidget>(
this,
_st.searchRow.height);
auto wrap = _searchView.get();
registerUpdateControlCallback(wrap, [=](anim::type) {
wrap->setVisible(!selectionMode() && _searchModeAvailable);
});
_searchField = field;
auto fieldWrap = Ui::CreateChild<Ui::FadeWrap<Ui::InputField>>(
wrap,
object_ptr<Ui::InputField>::fromRaw(field),
st::infoTopBarScale);
fieldWrap->setDuration(st::infoTopBarDuration);
auto focusLifetime = field->lifetime().make_state<rpl::lifetime>();
registerUpdateControlCallback(fieldWrap, [=](anim::type animated) {
auto fieldShown = !selectionMode() && searchMode();
if (!fieldShown && field->hasFocus()) {
setFocus();
}
fieldWrap->toggle(fieldShown, animated);
if (fieldShown) {
*focusLifetime = field->shownValue()
| rpl::filter([](bool shown) { return shown; })
| rpl::take(1)
| rpl::on_next([=] { field->setFocus(); });
} else {
focusLifetime->destroy();
}
});
auto button = base::make_unique_q<Ui::IconButton>(this, _st.search);
auto search = button.get();
search->addClickHandler([=] { showSearch(); });
auto searchWrap = pushButton(std::move(button));
registerToggleControlCallback(searchWrap, [=] {
return !selectionMode()
&& _searchModeAvailable
&& !_searchModeEnabled;
});
auto cancel = Ui::CreateChild<Ui::CrossButton>(
wrap,
_st.searchRow.fieldCancel);
registerToggleControlCallback(cancel, [=] {
return !selectionMode() && searchMode();
});
cancel->addClickHandler([=] {
if (!field->getLastText().isEmpty()) {
field->setText(QString());
} else {
_searchModeEnabled = false;
updateControlsVisibility(anim::type::normal);
}
});
wrap->widthValue(
) | rpl::on_next([=](int newWidth) {
auto availableWidth = newWidth
- _st.searchRow.fieldCancelSkip;
fieldWrap->resizeToWidth(availableWidth);
fieldWrap->moveToLeft(
_st.searchRow.padding.left(),
_st.searchRow.padding.top());
cancel->moveToRight(0, 0);
}, wrap->lifetime());
widthValue(
) | rpl::on_next([=](int newWidth) {
auto left = _back
? _st.back.width
: _st.titlePosition.x();
wrap->setGeometryToLeft(
left,
0,
newWidth - left,
wrap->height(),
newWidth);
}, wrap->lifetime());
field->alive(
) | rpl::on_done([=] {
field->setParent(nullptr);
removeButton(search);
clearSearchField();
}, _searchView->lifetime());
_searchModeEnabled = !field->getLastText().isEmpty() || startsFocused;
updateControlsVisibility(anim::type::instant);
std::move(
shown
) | rpl::on_next([=](bool visible) {
auto alreadyInSearch = !field->getLastText().isEmpty();
_searchModeAvailable = visible || alreadyInSearch;
updateControlsVisibility(anim::type::instant);
}, wrap->lifetime());
}
void TopBar::showSearch() {
_searchModeEnabled = true;
updateControlsVisibility(anim::type::normal);
}
void TopBar::removeButton(not_null<Ui::RpWidget*> button) {
_buttons.erase(
std::remove(_buttons.begin(), _buttons.end(), button),
_buttons.end());
}
int TopBar::resizeGetHeight(int newWidth) {
updateControlsGeometry(newWidth);
return _st.height;
}
void TopBar::updateControlsGeometry(int newWidth) {
updateDefaultControlsGeometry(newWidth);
updateSelectionControlsGeometry(newWidth);
updateStoriesGeometry(newWidth);
}
void TopBar::updateDefaultControlsGeometry(int newWidth) {
auto right = 0;
for (auto &button : _buttons) {
if (!button) {
continue;
}
button->moveToRight(right, 0, newWidth);
right += button->width();
}
if (_back) {
_back->setGeometryToLeft(
0,
0,
newWidth - right,
_back->height(),
newWidth);
}
if (_title) {
const auto x = _back
? _st.back.width
: _subtitle
? _st.titleWithSubtitlePosition.x()
: _st.titlePosition.x();
const auto y = _subtitle
? _st.titleWithSubtitlePosition.y()
: _st.titlePosition.y();
_title->moveToLeft(x, y, newWidth);
if (_subtitle) {
_subtitle->moveToLeft(
_back ? _st.back.width : _st.subtitlePosition.x(),
_st.subtitlePosition.y(),
newWidth);
}
}
}
void TopBar::updateSelectionControlsGeometry(int newWidth) {
if (!_selectionText) {
return;
}
auto right = _st.mediaActionsSkip;
if (_canDelete) {
_delete->moveToRight(right, 0, newWidth);
right += _delete->width();
}
if (_canToggleStoryPin) {
_toggleStoryInProfile->moveToRight(right, 0, newWidth);
right += _toggleStoryInProfile->width();
_toggleStoryPin->moveToRight(right, 0, newWidth);
right += _toggleStoryPin->width();
}
if (_canForward) {
_forward->moveToRight(right, 0, newWidth);
right += _forward->width();
}
auto left = 0;
_cancelSelection->moveToLeft(left, 0);
left += _cancelSelection->width();
const auto top = 0;
const auto availableWidth = newWidth - left - right;
_selectionText->resizeToNaturalWidth(availableWidth);
_selectionText->moveToLeft(
left,
top,
newWidth);
}
void TopBar::updateStoriesGeometry(int newWidth) {
if (!_stories) {
return;
}
auto right = 0;
for (auto &button : _buttons) {
if (!button) {
continue;
}
button->moveToRight(right, 0, newWidth);
right += button->width();
}
const auto &small = st::dialogsStories;
const auto wrapLeft = (_back ? _st.back.width : 0);
const auto left = _back
? 0
: (_st.titlePosition.x() - small.left - small.photoLeft);
const auto height = small.photo + 2 * small.photoTop;
const auto top = _st.titlePosition.y()
+ (_st.title.style.font->height - height) / 2;
_stories->setLayoutConstraints({ left, top }, style::al_left);
_storiesWrap->move(wrapLeft, 0);
}
void TopBar::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
auto highlight = _a_highlight.value(_highlight ? 1. : 0.);
if (_highlight && !_a_highlight.animating()) {
_highlight = false;
startHighlightAnimation();
}
if (!_roundRect) {
const auto brush = anim::brush(_st.bg, _st.highlightBg, highlight);
p.fillRect(e->rect(), brush);
} else if (highlight > 0.) {
p.setPen(Qt::NoPen);
p.setBrush(anim::brush(_st.bg, _st.highlightBg, highlight));
p.drawRoundedRect(
rect() + style::margins(0, 0, 0, _st.radius * 2),
_st.radius,
_st.radius);
} else {
_roundRect->paintSomeRounded(
p,
rect(),
RectPart::TopLeft | RectPart::TopRight);
}
}
void TopBar::highlight() {
_highlight = true;
startHighlightAnimation();
}
void TopBar::startHighlightAnimation() {
_a_highlight.start(
[this] { update(); },
_highlight ? 0. : 1.,
_highlight ? 1. : 0.,
_st.highlightDuration);
}
void TopBar::updateControlsVisibility(anim::type animated) {
for (auto i = _updateControlCallbacks.begin(); i != _updateControlCallbacks.end();) {
auto &&[widget, callback] = *i;
if (!callback(animated)) {
i = _updateControlCallbacks.erase(i);
} else {
++i;
}
}
}
void TopBar::setStories(rpl::producer<Dialogs::Stories::Content> content) {
_storiesLifetime.destroy();
delete _storiesWrap.data();
if (content) {
using namespace Dialogs::Stories;
auto last = std::move(
content
) | rpl::start_spawning(_storiesLifetime);
_storiesWrap = _storiesLifetime.make_state<
Ui::FadeWrap<Ui::AbstractButton>
>(this, object_ptr<Ui::AbstractButton>(this), st::infoTopBarScale);
registerToggleControlCallback(
_storiesWrap.data(),
[this] { return _storiesCount > 0; });
_storiesWrap->toggle(false, anim::type::instant);
_storiesWrap->setDuration(st::infoTopBarDuration);
const auto button = _storiesWrap->entity();
const auto stories = Ui::CreateChild<List>(
button,
st::dialogsStoriesListInfo,
rpl::duplicate(
last
) | rpl::filter([](const Content &content) {
return !content.elements.empty();
}));
const auto label = Ui::CreateChild<Ui::FlatLabel>(
button,
QString(),
_st.title);
stories->setAttribute(Qt::WA_TransparentForMouseEvents);
label->setAttribute(Qt::WA_TransparentForMouseEvents);
stories->geometryValue(
) | rpl::on_next([=](QRect geometry) {
const auto skip = _st.title.style.font->spacew;
label->move(
geometry.x() + geometry.width() + skip,
_st.titlePosition.y());
}, label->lifetime());
rpl::combine(
_storiesWrap->positionValue(),
label->geometryValue()
) | rpl::on_next([=] {
button->resize(
label->x() + label->width() + _st.titlePosition.x(),
_st.height);
}, button->lifetime());
_stories = stories;
_stories->clicks(
) | rpl::start_to_stream(_storyClicks, _stories->lifetime());
button->setClickedCallback([=] {
_storyClicks.fire({});
});
rpl::duplicate(
last
) | rpl::on_next([=](const Content &content) {
const auto count = content.total;
if (_storiesCount != count) {
const auto was = (_storiesCount > 0);
_storiesCount = count;
const auto now = (_storiesCount > 0);
if (was != now) {
updateControlsVisibility(anim::type::normal);
}
if (now) {
label->setText(
tr::lng_contacts_stories_status(
tr::now,
lt_count,
_storiesCount));
}
updateControlsGeometry(width());
}
}, _storiesLifetime);
_storiesLifetime.add([weak = base::make_weak(label)] {
delete weak.get();
});
} else {
_storiesCount = 0;
}
updateControlsVisibility(anim::type::instant);
}
void TopBar::setSelectedItems(SelectedItems &&items) {
auto wasSelectionMode = selectionMode();
_selectedItems = std::move(items);
if (selectionMode()) {
if (_selectionText) {
updateSelectionState();
if (!wasSelectionMode) {
_selectionText->entity()->finishAnimating();
}
} else {
createSelectionControls();
}
}
updateControlsVisibility(anim::type::normal);
}
SelectedItems TopBar::takeSelectedItems() {
_canDelete = false;
_canForward = false;
return std::move(_selectedItems);
}
rpl::producer<SelectionAction> TopBar::selectionActionRequests() const {
return _selectionActionRequests.events();
}
void TopBar::updateSelectionState() {
Expects(_selectionText
&& _delete
&& _forward
&& _toggleStoryInProfile
&& _toggleStoryPin);
_canDelete = computeCanDelete();
_canForward = computeCanForward();
_canUnpinStories = computeCanUnpinStories();
_canToggleStoryPin = computeCanToggleStoryPin();
_allStoriesInProfile = computeAllStoriesInProfile();
_selectionText->entity()->setValue(generateSelectedText());
_delete->toggle(_canDelete, anim::type::instant);
_forward->toggle(_canForward, anim::type::instant);
_toggleStoryInProfile->toggle(_canToggleStoryPin, anim::type::instant);
_toggleStoryInProfile->entity()->setIconOverride(
(_allStoriesInProfile
? &_st.storiesArchive.icon
: &_st.storiesSave.icon),
(_allStoriesInProfile
? &_st.storiesArchive.iconOver
: &_st.storiesSave.iconOver));
_toggleStoryPin->toggle(_canToggleStoryPin, anim::type::instant);
_toggleStoryPin->entity()->setIconOverride(
_canUnpinStories ? &_st.storiesUnpin.icon : nullptr,
_canUnpinStories ? &_st.storiesUnpin.iconOver : nullptr);
updateSelectionControlsGeometry(width());
}
void TopBar::createSelectionControls() {
auto wrap = [&](auto created) {
registerToggleControlCallback(
created,
[this] { return selectionMode(); });
created->toggle(false, anim::type::instant);
return created;
};
_canDelete = computeCanDelete();
_canForward = computeCanForward();
_canUnpinStories = computeCanUnpinStories();
_canToggleStoryPin = computeCanToggleStoryPin();
_allStoriesInProfile = computeAllStoriesInProfile();
_cancelSelection = wrap(Ui::CreateChild<Ui::FadeWrap<Ui::IconButton>>(
this,
object_ptr<Ui::IconButton>(this, _st.mediaCancel),
st::infoTopBarScale));
_cancelSelection->setDuration(st::infoTopBarDuration);
_cancelSelection->entity()->clicks(
) | rpl::map_to(
SelectionAction::Clear
) | rpl::start_to_stream(
_selectionActionRequests,
_cancelSelection->lifetime());
_selectionText = wrap(Ui::CreateChild<Ui::FadeWrap<Ui::LabelWithNumbers>>(
this,
object_ptr<Ui::LabelWithNumbers>(
this,
_st.title,
_st.titlePosition.y(),
generateSelectedText()),
st::infoTopBarScale));
_selectionText->setDuration(st::infoTopBarDuration);
_selectionText->entity()->resize(0, _st.height);
_selectionText->naturalWidthValue(
) | rpl::skip(1) | rpl::on_next([=] {
updateSelectionControlsGeometry(width());
}, _selectionText->lifetime());
_forward = wrap(Ui::CreateChild<Ui::FadeWrap<Ui::IconButton>>(
this,
object_ptr<Ui::IconButton>(this, _st.mediaForward),
st::infoTopBarScale));
registerToggleControlCallback(
_forward.data(),
[this] { return selectionMode() && _canForward; });
_forward->setDuration(st::infoTopBarDuration);
_forward->entity()->clicks(
) | rpl::map_to(
SelectionAction::Forward
) | rpl::start_to_stream(
_selectionActionRequests,
_cancelSelection->lifetime());
_forward->entity()->setVisible(_canForward);
_delete = wrap(Ui::CreateChild<Ui::FadeWrap<Ui::IconButton>>(
this,
object_ptr<Ui::IconButton>(this, _st.mediaDelete),
st::infoTopBarScale));
registerToggleControlCallback(
_delete.data(),
[this] { return selectionMode() && _canDelete; });
_delete->setDuration(st::infoTopBarDuration);
_delete->entity()->clicks(
) | rpl::map_to(
SelectionAction::Delete
) | rpl::start_to_stream(
_selectionActionRequests,
_cancelSelection->lifetime());
_delete->entity()->setVisible(_canDelete);
_toggleStoryInProfile = wrap(
Ui::CreateChild<Ui::FadeWrap<Ui::IconButton>>(
this,
object_ptr<Ui::IconButton>(
this,
_allStoriesInProfile ? _st.storiesArchive : _st.storiesSave),
st::infoTopBarScale));
registerToggleControlCallback(
_toggleStoryInProfile.data(),
[this] { return selectionMode() && _canToggleStoryPin; });
_toggleStoryInProfile->setDuration(st::infoTopBarDuration);
_toggleStoryInProfile->entity()->clicks(
) | rpl::map([=] {
return _allStoriesInProfile
? SelectionAction::ToggleStoryToArchive
: SelectionAction::ToggleStoryToProfile;
}) | rpl::start_to_stream(
_selectionActionRequests,
_cancelSelection->lifetime());
_toggleStoryInProfile->entity()->setVisible(_canToggleStoryPin);
_toggleStoryPin = wrap(
Ui::CreateChild<Ui::FadeWrap<Ui::IconButton>>(
this,
object_ptr<Ui::IconButton>(
this,
_st.storiesPin),
st::infoTopBarScale));
if (_canUnpinStories) {
_toggleStoryPin->entity()->setIconOverride(
_canUnpinStories ? &_st.storiesUnpin.icon : nullptr,
_canUnpinStories ? &_st.storiesUnpin.iconOver : nullptr);
}
registerToggleControlCallback(
_toggleStoryPin.data(),
[this] { return selectionMode() && _canToggleStoryPin; });
_toggleStoryPin->setDuration(st::infoTopBarDuration);
_toggleStoryPin->entity()->clicks(
) | rpl::map_to(
SelectionAction::ToggleStoryPin
) | rpl::start_to_stream(
_selectionActionRequests,
_cancelSelection->lifetime());
_toggleStoryPin->entity()->setVisible(_canToggleStoryPin);
updateControlsGeometry(width());
}
bool TopBar::computeCanDelete() const {
return ranges::all_of(_selectedItems.list, &SelectedItem::canDelete);
}
bool TopBar::computeCanForward() const {
return ranges::all_of(_selectedItems.list, &SelectedItem::canForward);
}
bool TopBar::computeCanUnpinStories() const {
return ranges::any_of(_selectedItems.list, &SelectedItem::canUnpinStory);
}
bool TopBar::computeCanToggleStoryPin() const {
return ranges::all_of(
_selectedItems.list,
&SelectedItem::canToggleStoryPin);
}
bool TopBar::computeAllStoriesInProfile() const {
return ranges::all_of(
_selectedItems.list,
&SelectedItem::storyInProfile);
}
Ui::StringWithNumbers TopBar::generateSelectedText() const {
return _selectedItems.title(_selectedItems.list.size());
}
bool TopBar::selectionMode() const {
return !_selectedItems.list.empty();
}
bool TopBar::storiesTitle() const {
return _storiesCount > 0;
}
bool TopBar::searchMode() const {
return _searchModeAvailable && _searchModeEnabled;
}
void TopBar::performForward() {
_selectionActionRequests.fire(SelectionAction::Forward);
}
void TopBar::performDelete() {
_selectionActionRequests.fire(SelectionAction::Delete);
}
} // namespace Info

View File

@@ -0,0 +1,200 @@
/*
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/rp_widget.h"
#include "ui/round_rect.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/effects/animations.h"
#include "ui/effects/numbers_animation.h"
#include "info/info_wrap_widget.h"
namespace style {
struct InfoTopBar;
} // namespace style
namespace Dialogs::Stories {
class List;
struct Content;
} // namespace Dialogs::Stories
namespace Window {
class SessionNavigation;
} // namespace Window
namespace Ui {
class AbstractButton;
class IconButton;
class FlatLabel;
class InputField;
class SearchFieldController;
class LabelWithNumbers;
} // namespace Ui
namespace Info {
class Key;
class Section;
struct TitleDescriptor {
rpl::producer<QString> title;
rpl::producer<QString> subtitle;
};
class TopBar : public Ui::RpWidget {
public:
TopBar(
QWidget *parent,
not_null<Window::SessionNavigation*> navigation,
const style::InfoTopBar &st,
SelectedItems &&items);
[[nodiscard]] auto backRequest() const {
return _backClicks.events();
}
[[nodiscard]] auto storyClicks() const {
return _storyClicks.events();
}
void setTitle(TitleDescriptor descriptor);
void setStories(rpl::producer<Dialogs::Stories::Content> content);
void enableBackButton();
void highlight();
template <typename ButtonWidget>
ButtonWidget *addButton(base::unique_qptr<ButtonWidget> button) {
auto result = button.get();
pushButton(std::move(button));
return result;
}
template <typename ButtonWidget>
ButtonWidget *addButtonWithVisibility(
base::unique_qptr<ButtonWidget> button,
rpl::producer<bool> shown) {
auto result = button.get();
forceButtonVisibility(
pushButton(std::move(button)),
std::move(shown));
return result;
}
void createSearchView(
not_null<Ui::SearchFieldController*> controller,
rpl::producer<bool> &&shown,
bool startsFocused);
bool focusSearchField();
void setSelectedItems(SelectedItems &&items);
SelectedItems takeSelectedItems();
[[nodiscard]] auto selectionActionRequests() const
-> rpl::producer<SelectionAction>;
void finishAnimating() {
updateControlsVisibility(anim::type::instant);
}
void showSearch();
void checkBeforeCloseByEscape(Fn<void()> close);
protected:
int resizeGetHeight(int newWidth) override;
void paintEvent(QPaintEvent *e) override;
private:
void updateControlsGeometry(int newWidth);
void updateDefaultControlsGeometry(int newWidth);
void updateSelectionControlsGeometry(int newWidth);
void updateStoriesGeometry(int newWidth);
Ui::FadeWrap<Ui::RpWidget> *pushButton(
base::unique_qptr<Ui::RpWidget> button);
void forceButtonVisibility(
Ui::FadeWrap<Ui::RpWidget> *button,
rpl::producer<bool> shown);
void removeButton(not_null<Ui::RpWidget*> button);
void startHighlightAnimation();
void updateControlsVisibility(anim::type animated);
[[nodiscard]] bool selectionMode() const;
[[nodiscard]] bool storiesTitle() const;
[[nodiscard]] bool searchMode() const;
[[nodiscard]] Ui::StringWithNumbers generateSelectedText() const;
[[nodiscard]] bool computeCanDelete() const;
[[nodiscard]] bool computeCanForward() const;
[[nodiscard]] bool computeCanUnpinStories() const;
[[nodiscard]] bool computeCanToggleStoryPin() const;
[[nodiscard]] bool computeAllStoriesInProfile() const;
void updateSelectionState();
void createSelectionControls();
void performForward();
void performDelete();
void performToggleStoryPin();
void setSearchField(
base::unique_qptr<Ui::InputField> field,
rpl::producer<bool> &&shown,
bool startsFocused);
void clearSearchField();
void createSearchView(
not_null<Ui::InputField*> field,
rpl::producer<bool> &&shown,
bool startsFocused);
template <typename Callback>
void registerUpdateControlCallback(QObject *guard, Callback &&callback);
template <typename Widget, typename IsVisible>
void registerToggleControlCallback(Widget *widget, IsVisible &&callback);
const not_null<Window::SessionNavigation*> _navigation;
const style::InfoTopBar &_st;
std::optional<Ui::RoundRect> _roundRect;
Ui::Animations::Simple _a_highlight;
bool _highlight = false;
QPointer<Ui::FadeWrap<Ui::IconButton>> _back;
std::vector<base::unique_qptr<Ui::RpWidget>> _buttons;
QPointer<Ui::FadeWrap<Ui::FlatLabel>> _title;
QPointer<Ui::FadeWrap<Ui::FlatLabel>> _subtitle;
bool _searchModeEnabled = false;
bool _searchModeAvailable = false;
base::unique_qptr<Ui::RpWidget> _searchView;
QPointer<Ui::InputField> _searchField;
rpl::event_stream<> _backClicks;
rpl::event_stream<uint64> _storyClicks;
SelectedItems _selectedItems;
bool _canDelete = false;
bool _canForward = false;
bool _canToggleStoryPin = false;
bool _canUnpinStories = false;
bool _allStoriesInProfile = false;
QPointer<Ui::FadeWrap<Ui::IconButton>> _cancelSelection;
QPointer<Ui::FadeWrap<Ui::LabelWithNumbers>> _selectionText;
QPointer<Ui::FadeWrap<Ui::IconButton>> _forward;
QPointer<Ui::FadeWrap<Ui::IconButton>> _delete;
QPointer<Ui::FadeWrap<Ui::IconButton>> _toggleStoryInProfile;
QPointer<Ui::FadeWrap<Ui::IconButton>> _toggleStoryPin;
rpl::event_stream<SelectionAction> _selectionActionRequests;
QPointer<Ui::FadeWrap<Ui::AbstractButton>> _storiesWrap;
QPointer<Dialogs::Stories::List> _stories;
rpl::lifetime _storiesLifetime;
int _storiesCount = 0;
using UpdateCallback = Fn<bool(anim::type)>;
std::map<QObject*, UpdateCallback> _updateControlCallbacks;
};
} // namespace Info

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,251 @@
/*
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 "window/section_widget.h"
#include "ui/effects/animations.h"
namespace Storage {
enum class SharedMediaType : signed char;
} // namespace Storage
namespace Ui {
namespace Controls {
struct SwipeHandlerArgs;
} // namespace Controls
class FadeShadow;
class PlainShadow;
class PopupMenu;
class IconButton;
class RoundRect;
struct StringWithNumbers;
} // namespace Ui
namespace Window {
enum class SlideDirection;
} // namespace Window
namespace Info {
namespace Profile {
class Widget;
} // namespace Profile
namespace Media {
class Widget;
} // namespace Media
class Key;
class Controller;
class Section;
class Memento;
class MoveMemento;
class ContentMemento;
class ContentWidget;
class TopBar;
enum class Wrap {
Layer,
Narrow,
Side,
Search,
StoryAlbumEdit,
};
struct SelectedItem {
explicit SelectedItem(GlobalMsgId globalId) : globalId(globalId) {
}
GlobalMsgId globalId;
bool canDelete = false;
bool canForward = false;
bool canToggleStoryPin = false;
bool canUnpinStory = false;
bool storyInProfile = false;
};
struct SelectedItems {
SelectedItems() = default;
explicit SelectedItems(Storage::SharedMediaType type);
Fn<Ui::StringWithNumbers(int)> title;
std::vector<SelectedItem> list;
};
enum class SelectionAction {
Clear,
Forward,
Delete,
ToggleStoryPin,
ToggleStoryToProfile,
ToggleStoryToArchive,
};
class WrapWidget final : public Window::SectionWidget {
public:
WrapWidget(
QWidget *parent,
not_null<Window::SessionController*> window,
Wrap wrap,
not_null<Memento*> memento);
Key key() const;
Dialogs::RowDescriptor activeChat() const override;
Wrap wrap() const {
return _wrap.current();
}
rpl::producer<Wrap> wrapValue() const;
void setWrap(Wrap wrap);
rpl::producer<> contentChanged() const;
not_null<Controller*> controller() {
return _controller.get();
}
bool hasTopBarShadow() const override;
QPixmap grabForShowAnimation(
const Window::SectionSlideParams &params) override;
void forceContentRepaint();
bool showInternal(
not_null<Window::SectionMemento*> memento,
const Window::SectionShow &params) override;
bool showBackFromStackInternal(const Window::SectionShow &params);
void removeFromStack(const std::vector<Section> &sections);
std::shared_ptr<Window::SectionMemento> createMemento() override;
rpl::producer<int> desiredHeightValue() const override;
// Float player interface.
bool floatPlayerHandleWheelEvent(QEvent *e) override;
QRect floatPlayerAvailableRect() override;
object_ptr<Ui::RpWidget> createTopBarSurrogate(QWidget *parent);
[[nodiscard]] bool hasBackButton() const;
[[nodiscard]] bool closeByOutsideClick() const;
void updateGeometry(
QRect newGeometry,
bool expanding,
int additionalScroll,
int maxVisibleHeight);
[[nodiscard]] int scrollBottomSkip() const;
[[nodiscard]] int scrollTillBottom(int forHeight) const;
[[nodiscard]] rpl::producer<int> scrollTillBottomChanges() const;
[[nodiscard]] rpl::producer<bool> grabbingForExpanding() const;
[[nodiscard]] const Ui::RoundRect *bottomSkipRounding() const;
[[nodiscard]] rpl::producer<> removeRequests() const override {
return _removeRequests.events();
}
[[nodiscard]] rpl::producer<SelectedItems> selectedListValue() const;
void replaceSwipeHandler(Ui::Controls::SwipeHandlerArgs *incompleteArgs);
~WrapWidget();
protected:
void resizeEvent(QResizeEvent *e) override;
void keyPressEvent(QKeyEvent *e) override;
void doSetInnerFocus() override;
void showFinishedHook() override;
void showAnimatedHook(
const Window::SectionSlideParams &params) override;
private:
using SlideDirection = Window::SlideDirection;
using SectionSlideParams = Window::SectionSlideParams;
struct StackItem;
void startInjectingActivePeerProfiles();
void injectActiveProfile(Dialogs::Key key);
void injectActivePeerProfile(not_null<PeerData*> peer);
void injectActiveProfileMemento(
std::shared_ptr<ContentMemento> memento);
void checkBeforeClose(Fn<void()> close);
void checkBeforeCloseByEscape(Fn<void()> close);
void restoreHistoryStack(
std::vector<std::shared_ptr<ContentMemento>> stack);
bool hasStackHistory() const {
return !_historyStack.empty();
}
void showNewContent(not_null<ContentMemento*> memento);
void showNewContent(
not_null<ContentMemento*> memento,
const Window::SectionShow &params);
bool returnToFirstStackFrame(
not_null<ContentMemento*> memento,
const Window::SectionShow &params);
void setupTop();
void setupTopBarMenuToggle();
void createTopBar();
void highlightTopBar();
void setupShortcuts();
[[nodiscard]] bool willHaveBackButton(
const Window::SectionShow &params) const;
not_null<RpWidget*> topWidget() const;
QRect contentGeometry() const;
rpl::producer<int> desiredHeightForContent() const;
void finishShowContent();
rpl::producer<bool> topShadowToggledValue() const;
void updateContentGeometry();
void showContent(object_ptr<ContentWidget> content);
object_ptr<ContentWidget> createContent(
not_null<ContentMemento*> memento,
not_null<Controller*> controller);
std::unique_ptr<Controller> createController(
not_null<Window::SessionController*> window,
not_null<ContentMemento*> memento);
bool requireTopBarSearch() const;
void addTopBarMenuButton();
void addProfileCallsButton();
void showTopBarMenu(bool check);
const bool _isSeparatedWindow = false;
rpl::variable<Wrap> _wrap;
std::unique_ptr<Controller> _controller;
object_ptr<ContentWidget> _content = { nullptr };
int _additionalScroll = 0;
int _maxVisibleHeight = 0;
bool _expanding = false;
rpl::variable<bool> _grabbingForExpanding = false;
object_ptr<TopBar> _topBar = { nullptr };
object_ptr<Ui::RpWidget> _topBarSurrogate = { nullptr };
Ui::Animations::Simple _topBarOverrideAnimation;
bool _topBarOverrideShown = false;
object_ptr<Ui::FadeShadow> _topShadow;
object_ptr<Ui::FadeShadow> _bottomShadow;
base::unique_qptr<Ui::IconButton> _topBarMenuToggle;
base::unique_qptr<Ui::PopupMenu> _topBarMenu;
std::vector<StackItem> _historyStack;
rpl::event_stream<> _removeRequests;
rpl::event_stream<rpl::producer<int>> _desiredHeights;
rpl::event_stream<rpl::producer<bool>> _desiredShadowVisibilities;
rpl::event_stream<rpl::producer<bool>> _desiredBottomShadowVisibilities;
rpl::event_stream<rpl::producer<SelectedItems>> _selectedLists;
rpl::event_stream<rpl::producer<int>> _scrollTillBottomChanges;
rpl::event_stream<> _contentChanges;
};
} // namespace Info

View File

@@ -0,0 +1,399 @@
/*
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 "info/media/info_media_buttons.h"
#include "base/call_delayed.h"
#include "base/qt/qt_key_modifiers.h"
#include "core/application.h"
#include "core/ui_integration.h"
#include "data/components/recent_shared_media_gifts.h"
#include "data/data_channel.h"
#include "data/data_document.h"
#include "data/data_saved_messages.h"
#include "data/data_saved_sublist.h"
#include "data/data_session.h"
#include "data/data_stories_ids.h"
#include "data/data_user.h"
#include "data/stickers/data_custom_emoji.h"
#include "history/history.h"
#include "history/view/history_view_chat_section.h"
#include "info/info_controller.h"
#include "info/info_memento.h"
#include "info/peer_gifts/info_peer_gifts_widget.h"
#include "info/profile/info_profile_values.h"
#include "info/saved/info_saved_music_widget.h"
#include "info/stories/info_stories_widget.h"
#include "main/main_session.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/popup_menu.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "window/window_separate_id.h"
#include "window/window_session_controller.h"
#include "styles/style_info.h"
#include "styles/style_menu_icons.h"
namespace Info::Media {
namespace {
[[nodiscard]] bool SeparateSupported(Storage::SharedMediaType type) {
using Type = Storage::SharedMediaType;
return (type == Type::Photo)
|| (type == Type::Video)
|| (type == Type::File)
|| (type == Type::MusicFile)
|| (type == Type::Link)
|| (type == Type::RoundVoiceFile)
|| (type == Type::GIF);
}
[[nodiscard]] Window::SeparateId SeparateId(
not_null<PeerData*> peer,
MsgId topicRootId,
Storage::SharedMediaType type) {
if (peer->isSelf() || !SeparateSupported(type)) {
return { nullptr };
}
const auto topic = topicRootId
? peer->forumTopicFor(topicRootId)
: nullptr;
if (topicRootId && !topic) {
return { nullptr };
}
const auto thread = topic
? (Data::Thread*)topic
: peer->owner().history(peer);
return { thread, type };
}
void AddContextMenuToButton(
not_null<Ui::AbstractButton*> button,
Fn<void()> openInWindow) {
if (!openInWindow) {
return;
}
button->setAcceptBoth();
struct State final {
base::unique_qptr<Ui::PopupMenu> menu;
};
const auto state = button->lifetime().make_state<State>();
button->addClickHandler([=](Qt::MouseButton mouse) {
if (mouse != Qt::RightButton) {
return;
}
state->menu = base::make_unique_q<Ui::PopupMenu>(
button.get(),
st::popupMenuWithIcons);
state->menu->addAction(tr::lng_context_new_window(tr::now), [=] {
base::call_delayed(
st::popupMenuWithIcons.showDuration,
crl::guard(button, openInWindow));
}, &st::menuIconNewWindow);
state->menu->popup(QCursor::pos());
});
}
} // namespace
tr::phrase<lngtag_count> MediaTextPhrase(Type type) {
switch (type) {
case Type::Photo: return tr::lng_profile_photos;
case Type::GIF: return tr::lng_profile_gifs;
case Type::Video: return tr::lng_profile_videos;
case Type::File: return tr::lng_profile_files;
case Type::MusicFile: return tr::lng_profile_songs;
case Type::Link: return tr::lng_profile_shared_links;
case Type::RoundVoiceFile: return tr::lng_profile_audios;
}
Unexpected("Type in MediaTextPhrase()");
};
Fn<QString(int)> MediaText(Type type) {
return [phrase = MediaTextPhrase(type)](int count) {
return phrase(tr::now, lt_count, count);
};
}
not_null<Ui::SlideWrap<Ui::SettingsButton>*> AddCountedButton(
Ui::VerticalLayout *parent,
rpl::producer<int> &&count,
Fn<QString(int)> &&textFromCount,
Ui::MultiSlideTracker &tracker) {
using namespace ::Settings;
auto forked = std::move(count)
| start_spawning(parent->lifetime());
auto text = rpl::duplicate(
forked
) | rpl::map([textFromCount](int count) {
return (count > 0)
? textFromCount(count)
: QString();
});
auto button = parent->add(object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
parent,
object_ptr<Ui::SettingsButton>(
parent,
std::move(text),
st::infoSharedMediaButton))
)->setDuration(
st::infoSlideDuration
)->toggleOn(
rpl::duplicate(forked) | rpl::map(rpl::mappers::_1 > 0)
);
tracker.track(button);
return button;
};
not_null<Ui::SettingsButton*> AddButton(
Ui::VerticalLayout *parent,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
MsgId topicRootId,
PeerId monoforumPeerId,
PeerData *migrated,
Type type,
Ui::MultiSlideTracker &tracker) {
auto result = AddCountedButton(
parent,
Profile::SharedMediaCountValue(
peer,
topicRootId,
monoforumPeerId,
migrated,
type),
MediaText(type),
tracker)->entity();
const auto separateId = SeparateId(peer, topicRootId, type);
const auto openInWindow = separateId
? [=] { navigation->parentController()->showInNewWindow(separateId); }
: Fn<void()>(nullptr);
AddContextMenuToButton(result, openInWindow);
result->addClickHandler([=](Qt::MouseButton mouse) {
if (mouse == Qt::RightButton) {
return;
}
if (openInWindow
&& (base::IsCtrlPressed() || mouse == Qt::MiddleButton)) {
return openInWindow();
}
const auto topic = topicRootId
? peer->forumTopicFor(topicRootId)
: nullptr;
if (topicRootId && !topic) {
return;
}
const auto separateId = SeparateId(peer, topicRootId, type);
if (Core::App().separateWindowFor(separateId) && openInWindow) {
openInWindow();
} else {
navigation->showSection(topicRootId
? std::make_shared<Info::Memento>(topic, Section(type))
: std::make_shared<Info::Memento>(peer, Section(type)));
}
});
return result;
};
not_null<Ui::SettingsButton*> AddCommonGroupsButton(
Ui::VerticalLayout *parent,
not_null<Window::SessionNavigation*> navigation,
not_null<UserData*> user,
Ui::MultiSlideTracker &tracker) {
auto result = AddCountedButton(
parent,
Profile::CommonGroupsCountValue(user),
[](int count) {
return tr::lng_profile_common_groups(tr::now, lt_count, count);
},
tracker)->entity();
result->addClickHandler([=] {
navigation->showSection(
std::make_shared<Info::Memento>(
user,
Section::Type::CommonGroups));
});
return result;
}
not_null<Ui::SettingsButton*> AddSimilarPeersButton(
Ui::VerticalLayout *parent,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
Ui::MultiSlideTracker &tracker) {
auto result = AddCountedButton(
parent,
Profile::SimilarPeersCountValue(peer),
[=](int count) {
return peer->isBroadcast()
? tr::lng_profile_similar_channels(tr::now, lt_count, count)
: tr::lng_profile_similar_bots(tr::now, lt_count, count);
},
tracker)->entity();
result->addClickHandler([=] {
navigation->showSection(
std::make_shared<Info::Memento>(
peer,
Section::Type::SimilarPeers));
});
return result;
}
not_null<Ui::SettingsButton*> AddStoriesButton(
Ui::VerticalLayout *parent,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
Ui::MultiSlideTracker &tracker) {
auto count = rpl::single(0) | rpl::then(Data::AlbumStoriesIds(
peer,
0, // = Data::kStoriesAlbumIdSaved
ServerMaxStoryId - 1,
0
) | rpl::map([](const Data::StoriesIdsSlice &slice) {
return slice.fullCount().value_or(0);
}));
const auto phrase = peer->isChannel() ? (+[](int count) {
return tr::lng_profile_posts(tr::now, lt_count, count);
}) : (+[](int count) {
return tr::lng_profile_saved_stories(tr::now, lt_count, count);
});
auto result = AddCountedButton(
parent,
std::move(count),
phrase,
tracker)->entity();
result->addClickHandler([=] {
navigation->showSection(Info::Stories::Make(peer));
});
return result;
}
not_null<Ui::SettingsButton*> AddSavedSublistButton(
Ui::VerticalLayout *parent,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
Ui::MultiSlideTracker &tracker) {
auto result = AddCountedButton(
parent,
Profile::SavedSublistCountValue(peer),
[](int count) {
return tr::lng_profile_saved_messages(tr::now, lt_count, count);
},
tracker)->entity();
result->addClickHandler([=] {
using namespace HistoryView;
const auto sublist = peer->owner().savedMessages().sublist(peer);
navigation->showSection(
std::make_shared<ChatMemento>(ChatViewId{
.history = sublist->owningHistory(),
.sublist = sublist,
}));
});
return result;
}
not_null<Ui::SettingsButton*> AddPeerGiftsButton(
Ui::VerticalLayout *parent,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
Ui::MultiSlideTracker &tracker) {
auto count = Profile::PeerGiftsCountValue(peer);
auto textFromCount = [](int count) {
return tr::lng_profile_peer_gifts(tr::now, lt_count, count);
};
using namespace ::Settings;
auto forked = std::move(count)
| start_spawning(parent->lifetime());
auto text = rpl::duplicate(
forked
) | rpl::map([textFromCount](int count) {
return (count > 0)
? textFromCount(count)
: QString();
});
struct State final {
std::vector<std::unique_ptr<Ui::Text::CustomEmoji>> emojiList;
rpl::event_stream<> textRefreshed;
QPointer<Ui::SettingsButton> button;
rpl::lifetime appearedLifetime;
};
const auto state = parent->lifetime().make_state<State>();
const auto refresh = [=] {
if (state->button) {
state->button->update();
}
};
auto customs = state->textRefreshed.events(
) | rpl::map([=]() -> TextWithEntities {
auto result = TextWithEntities();
for (const auto &custom : state->emojiList) {
result.append(Ui::Text::SingleCustomEmoji(custom->entityData()));
}
return result;
});
const auto wrap = parent->add(
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
parent,
object_ptr<Ui::SettingsButton>(
parent,
rpl::combine(
std::move(text),
std::move(customs)
) | rpl::map([=](QString text, TextWithEntities customs) {
return TextWithEntities()
.append(std::move(text))
.append(QChar(' '))
.append(std::move(customs));
}),
st::infoSharedMediaButton,
Core::TextContext({
.session = &navigation->session(),
.details = { .session = &navigation->session() },
.repaint = refresh,
.customEmojiLoopLimit = 1,
}))));
wrap->setDuration(st::infoSlideDuration);
wrap->toggleOn(rpl::duplicate(forked) | rpl::map(rpl::mappers::_1 > 0));
tracker.track(wrap);
rpl::duplicate(forked) | rpl::filter(
rpl::mappers::_1 > 0
) | rpl::on_next([=] {
state->appearedLifetime.destroy();
const auto requestDone = crl::guard(wrap, [=](
std::vector<Data::SavedStarGift> gifts) {
state->emojiList.clear();
for (const auto &gift : gifts) {
state->emojiList.push_back(
peer->owner().customEmojiManager().create(
gift.info.document->id,
refresh));
}
state->textRefreshed.fire({});
});
navigation->session().recentSharedGifts().request(peer, requestDone);
}, state->appearedLifetime);
state->button = wrap->entity();
wrap->entity()->addClickHandler([=] {
if (navigation->showFrozenError()) {
return;
}
navigation->showSection(Info::PeerGifts::Make(peer));
});
return wrap->entity();
}
} // namespace Info::Media

View File

@@ -0,0 +1,80 @@
/*
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 "lang/lang_keys.h"
#include "storage/storage_shared_media.h"
namespace Ui {
class AbstractButton;
class MultiSlideTracker;
class SettingsButton;
class VerticalLayout;
template <typename Widget>
class SlideWrap;
} // namespace Ui
namespace Window {
class SessionNavigation;
} // namespace Window
namespace Info::Media {
using Type = Storage::SharedMediaType;
[[nodiscard]] tr::phrase<lngtag_count> MediaTextPhrase(Type type);
[[nodiscard]] Fn<QString(int)> MediaText(Type type);
[[nodiscard]] not_null<Ui::SlideWrap<Ui::SettingsButton>*> AddCountedButton(
Ui::VerticalLayout *parent,
rpl::producer<int> &&count,
Fn<QString(int)> &&textFromCount,
Ui::MultiSlideTracker &tracker);
[[nodiscard]] not_null<Ui::SettingsButton*> AddButton(
Ui::VerticalLayout *parent,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
MsgId topicRootId,
PeerId monoforumPeerId,
PeerData *migrated,
Type type,
Ui::MultiSlideTracker &tracker);
[[nodiscard]] not_null<Ui::SettingsButton*> AddCommonGroupsButton(
Ui::VerticalLayout *parent,
not_null<Window::SessionNavigation*> navigation,
not_null<UserData*> user,
Ui::MultiSlideTracker &tracker);
[[nodiscard]] not_null<Ui::SettingsButton*> AddSimilarPeersButton(
Ui::VerticalLayout *parent,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
Ui::MultiSlideTracker &tracker);
[[nodiscard]] not_null<Ui::SettingsButton*> AddStoriesButton(
Ui::VerticalLayout *parent,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
Ui::MultiSlideTracker &tracker);
[[nodiscard]] not_null<Ui::SettingsButton*> AddSavedSublistButton(
Ui::VerticalLayout *parent,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
Ui::MultiSlideTracker &tracker);
[[nodiscard]] not_null<Ui::SettingsButton*> AddPeerGiftsButton(
Ui::VerticalLayout *parent,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
Ui::MultiSlideTracker &tracker);
} // namespace Info::Media

View File

@@ -0,0 +1,94 @@
/*
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 "info/media/info_media_common.h"
#include "history/history_item.h"
#include "storage/storage_shared_media.h"
#include "styles/style_info.h"
#include "styles/style_overview.h"
namespace Info::Media {
UniversalMsgId GetUniversalId(FullMsgId itemId) {
return peerIsChannel(itemId.peer)
? UniversalMsgId(itemId.msg)
: UniversalMsgId(itemId.msg - ServerMaxMsgId);
}
UniversalMsgId GetUniversalId(not_null<const HistoryItem*> item) {
return GetUniversalId(item->fullId());
}
UniversalMsgId GetUniversalId(not_null<const BaseLayout*> layout) {
return GetUniversalId(layout->getItem()->fullId());
}
bool ChangeItemSelection(
ListSelectedMap &selected,
not_null<const HistoryItem*> item,
ListItemSelectionData selectionData,
int limit) {
if (!limit) {
limit = MaxSelectedItems;
}
const auto changeExisting = [&](auto it) {
if (it == selected.cend()) {
return false;
} else if (it->second != selectionData) {
it->second = selectionData;
return true;
}
return false;
};
if (selected.size() < limit) {
const auto &[i, ok] = selected.try_emplace(item, selectionData);
if (ok) {
return true;
}
return changeExisting(i);
}
return changeExisting(selected.find(item));
}
int MinItemHeight(Type type, int width) {
auto &songSt = st::overviewFileLayout;
switch (type) {
case Type::Photo:
case Type::GIF:
case Type::Video:
case Type::RoundFile: {
auto itemsLeft = st::infoMediaSkip;
auto itemsInRow = (width - itemsLeft)
/ (st::infoMediaMinGridSize + st::infoMediaSkip);
return (st::infoMediaMinGridSize + st::infoMediaSkip) / itemsInRow;
} break;
case Type::RoundVoiceFile:
return songSt.songPadding.top()
+ songSt.songThumbSize
+ songSt.songPadding.bottom()
+ st::lineWidth;
case Type::File:
return songSt.filePadding.top()
+ songSt.fileThumbSize
+ songSt.filePadding.bottom()
+ st::lineWidth;
case Type::MusicFile:
return songSt.songPadding.top()
+ songSt.songThumbSize
+ songSt.songPadding.bottom();
case Type::Link:
return st::linksPhotoSize
+ st::linksMargin.top()
+ st::linksMargin.bottom()
+ st::linksBorder;
}
Unexpected("Type in MinItemHeight()");
}
} // namespace Info::Media

View File

@@ -0,0 +1,185 @@
/*
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 "overview/overview_layout.h"
namespace Storage {
enum class SharedMediaType : signed char;
} // namespace Storage
namespace Info::Media {
using Type = Storage::SharedMediaType;
using BaseLayout = Overview::Layout::ItemBase;
class Memento;
class ListSection;
inline constexpr auto kPreloadIfLessThanScreens = 2;
struct ListItemSelectionData {
explicit ListItemSelectionData(TextSelection text) : text(text) {
}
TextSelection text;
bool canDelete = false;
bool canForward = false;
bool canToggleStoryPin = false;
bool canUnpinStory = false;
bool storyInProfile = false;
friend inline bool operator==(
ListItemSelectionData,
ListItemSelectionData) = default;
};
using ListSelectedMap = base::flat_map<
not_null<const HistoryItem*>,
ListItemSelectionData,
std::less<>>;
enum class ListDragSelectAction {
None,
Selecting,
Deselecting,
};
struct ListContext {
Overview::Layout::PaintContext layoutContext;
not_null<ListSelectedMap*> selected;
not_null<ListSelectedMap*> dragSelected;
ListDragSelectAction dragSelectAction = ListDragSelectAction::None;
BaseLayout *draggedItem = nullptr;
};
struct ListScrollTopState {
int64 position = 0; // ListProvider-specific.
HistoryItem *item = nullptr;
int shift = 0;
};
struct ListFoundItem {
not_null<BaseLayout*> layout;
QRect geometry;
bool exact = false;
};
struct ListFoundItemWithSection {
ListFoundItem item;
not_null<const ListSection*> section;
};
struct CachedItem {
CachedItem(std::unique_ptr<BaseLayout> item) : item(std::move(item)) {
};
CachedItem(CachedItem &&other) = default;
CachedItem &operator=(CachedItem &&other) = default;
~CachedItem() = default;
std::unique_ptr<BaseLayout> item;
bool stale = false;
};
using UniversalMsgId = MsgId;
[[nodiscard]] UniversalMsgId GetUniversalId(FullMsgId itemId);
[[nodiscard]] UniversalMsgId GetUniversalId(
not_null<const HistoryItem*> item);
[[nodiscard]] UniversalMsgId GetUniversalId(
not_null<const BaseLayout*> layout);
bool ChangeItemSelection(
ListSelectedMap &selected,
not_null<const HistoryItem*> item,
ListItemSelectionData selectionData,
int limit = 0);
class ListSectionDelegate {
public:
[[nodiscard]] virtual bool sectionHasFloatingHeader() = 0;
[[nodiscard]] virtual QString sectionTitle(
not_null<const BaseLayout*> item) = 0;
[[nodiscard]] virtual bool sectionItemBelongsHere(
not_null<const BaseLayout*> item,
not_null<const BaseLayout*> previous) = 0;
[[nodiscard]] not_null<ListSectionDelegate*> sectionDelegate() {
return this;
}
};
class ListProvider {
public:
[[nodiscard]] virtual Type type() = 0;
[[nodiscard]] virtual bool hasSelectRestriction() = 0;
[[nodiscard]] virtual auto hasSelectRestrictionChanges()
->rpl::producer<bool> = 0;
[[nodiscard]] virtual bool isPossiblyMyItem(
not_null<const HistoryItem*> item) = 0;
[[nodiscard]] virtual std::optional<int> fullCount() = 0;
virtual void restart() = 0;
virtual void checkPreload(
QSize viewport,
not_null<BaseLayout*> topLayout,
not_null<BaseLayout*> bottomLayout,
bool preloadTop,
bool preloadBottom) = 0;
virtual void refreshViewer() = 0;
[[nodiscard]] virtual rpl::producer<> refreshed() = 0;
[[nodiscard]] virtual std::vector<ListSection> fillSections(
not_null<Overview::Layout::Delegate*> delegate) = 0;
[[nodiscard]] virtual auto layoutRemoved()
-> rpl::producer<not_null<BaseLayout*>> = 0;
[[nodiscard]] virtual BaseLayout *lookupLayout(
const HistoryItem *item) = 0;
[[nodiscard]] virtual bool isMyItem(
not_null<const HistoryItem*> item) = 0;
[[nodiscard]] virtual bool isAfter(
not_null<const HistoryItem*> a,
not_null<const HistoryItem*> b) = 0;
[[nodiscard]] virtual ListItemSelectionData computeSelectionData(
not_null<const HistoryItem*> item,
TextSelection selection) = 0;
virtual void applyDragSelection(
ListSelectedMap &selected,
not_null<const HistoryItem*> fromItem,
bool skipFrom,
not_null<const HistoryItem*> tillItem,
bool skipTill) = 0;
[[nodiscard]] virtual bool allowSaveFileAs(
not_null<const HistoryItem*> item,
not_null<DocumentData*> document) = 0;
[[nodiscard]] virtual QString showInFolderPath(
not_null<const HistoryItem*> item,
not_null<DocumentData*> document) = 0;
virtual void setSearchQuery(QString query) = 0;
[[nodiscard]] virtual int64 scrollTopStatePosition(
not_null<HistoryItem*> item) = 0;
[[nodiscard]] virtual HistoryItem *scrollTopStateItem(
ListScrollTopState state) = 0;
virtual void saveState(
not_null<Memento*> memento,
ListScrollTopState scrollState) = 0;
virtual void restoreState(
not_null<Memento*> memento,
Fn<void(ListScrollTopState)> restoreScrollState) = 0;
virtual ~ListProvider() = default;
};
[[nodiscard]] int MinItemHeight(Type type, int width);
} // namespace Info::Media

View File

@@ -0,0 +1,106 @@
/*
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 "info/media/info_media_empty_widget.h"
#include "ui/widgets/labels.h"
#include "styles/style_info.h"
#include "lang/lang_keys.h"
namespace Info {
namespace Media {
EmptyWidget::EmptyWidget(QWidget *parent)
: RpWidget(parent)
, _text(this, st::infoEmptyLabel) {
}
void EmptyWidget::setFullHeight(rpl::producer<int> fullHeightValue) {
std::move(
fullHeightValue
) | rpl::on_next([this](int fullHeight) {
// Make icon center be on 1/3 height.
auto iconCenter = fullHeight / 3;
auto iconHeight = st::infoEmptyFile.height();
auto iconTop = iconCenter - iconHeight / 2;
_height = iconTop + st::infoEmptyIconTop;
resizeToWidth(width());
}, lifetime());
}
void EmptyWidget::setType(Type type) {
_type = type;
_icon = [&] {
switch (_type) {
case Type::Photo:
case Type::GIF: return &st::infoEmptyPhoto;
case Type::Video: return &st::infoEmptyVideo;
case Type::MusicFile: return &st::infoEmptyAudio;
case Type::File: return &st::infoEmptyFile;
case Type::Link: return &st::infoEmptyLink;
case Type::RoundVoiceFile: return &st::infoEmptyVoice;
}
Unexpected("Bad type in EmptyWidget::setType()");
}();
update();
}
void EmptyWidget::setSearchQuery(const QString &query) {
_text->setText([&] {
switch (_type) {
case Type::Photo:
return tr::lng_media_photo_empty(tr::now);
case Type::GIF:
return tr::lng_media_gif_empty(tr::now);
case Type::Video:
return tr::lng_media_video_empty(tr::now);
case Type::MusicFile:
return query.isEmpty()
? tr::lng_media_song_empty(tr::now)
: tr::lng_media_song_empty_search(tr::now);
case Type::File:
return query.isEmpty()
? tr::lng_media_file_empty(tr::now)
: tr::lng_media_file_empty_search(tr::now);
case Type::Link:
return query.isEmpty()
? tr::lng_media_link_empty(tr::now)
: tr::lng_media_link_empty_search(tr::now);
case Type::RoundVoiceFile:
return tr::lng_media_audio_empty(tr::now);
}
Unexpected("Bad type in EmptyWidget::setSearchQuery()");
}());
resizeToWidth(width());
}
void EmptyWidget::paintEvent(QPaintEvent *e) {
if (!_icon) {
return;
}
auto p = QPainter(this);
auto iconLeft = (width() - _icon->width()) / 2;
auto iconTop = height() - st::infoEmptyIconTop;
_icon->paint(p, iconLeft, iconTop, width());
}
int EmptyWidget::resizeGetHeight(int newWidth) {
auto labelTop = _height - st::infoEmptyLabelTop;
auto labelWidth = newWidth - 2 * st::infoEmptyLabelSkip;
_text->resizeToNaturalWidth(labelWidth);
auto labelLeft = (newWidth - _text->width()) / 2;
_text->moveToLeft(labelLeft, labelTop, newWidth);
update();
return _height;
}
} // namespace Media
} // namespace Info

View File

@@ -0,0 +1,42 @@
/*
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/rp_widget.h"
#include "info/media/info_media_widget.h"
namespace Ui {
class FlatLabel;
} // namespace Ui
namespace Info {
namespace Media {
class EmptyWidget : public Ui::RpWidget {
public:
EmptyWidget(QWidget *parent);
void setFullHeight(rpl::producer<int> fullHeightValue);
void setType(Type type);
void setSearchQuery(const QString &query);
protected:
void paintEvent(QPaintEvent *e) override;
int resizeGetHeight(int newWidth) override;
private:
object_ptr<Ui::FlatLabel> _text;
Type _type = Type::kCount;
const style::icon *_icon = nullptr;
int _height = 0;
};
} // namespace Media
} // namespace Info

View File

@@ -0,0 +1,249 @@
/*
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 "info/media/info_media_inner_widget.h"
#include <rpl/flatten_latest.h>
#include "boxes/abstract_box.h"
#include "info/media/info_media_list_widget.h"
#include "info/media/info_media_buttons.h"
#include "info/media/info_media_empty_widget.h"
#include "info/profile/info_profile_icon.h"
#include "info/info_controller.h"
#include "data/data_forum_topic.h"
#include "data/data_peer.h"
#include "data/data_saved_sublist.h"
#include "ui/widgets/discrete_sliders.h"
#include "ui/widgets/shadow.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/box_content_divider.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/search_field_controller.h"
#include "styles/style_info.h"
#include "lang/lang_keys.h"
namespace Info {
namespace Media {
InnerWidget::InnerWidget(
QWidget *parent,
not_null<Controller*> controller)
: RpWidget(parent)
, _controller(controller)
, _empty(this) {
_empty->heightValue(
) | rpl::on_next(
[this] { refreshHeight(); },
_empty->lifetime());
_list = setupList();
}
// Allows showing additional shared media links and tabs.
// Used for shared media in Saved Messages.
void InnerWidget::setupOtherTypes() {
if (_controller->key().peer()->sharedMediaInfo() && _isStackBottom) {
createOtherTypes();
} else {
_otherTypes.destroy();
refreshHeight();
}
}
void InnerWidget::createOtherTypes() {
_otherTypes.create(this);
_otherTypes->show();
createTypeButtons();
_otherTypes->add(object_ptr<Ui::BoxContentDivider>(_otherTypes));
_otherTypes->resizeToWidth(width());
_otherTypes->heightValue(
) | rpl::on_next(
[this] { refreshHeight(); },
_otherTypes->lifetime());
}
void InnerWidget::createTypeButtons() {
auto wrap = _otherTypes->add(object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
_otherTypes,
object_ptr<Ui::VerticalLayout>(_otherTypes)));
auto content = wrap->entity();
content->add(object_ptr<Ui::FixedHeightWidget>(
content,
st::infoProfileSkip));
auto tracker = Ui::MultiSlideTracker();
const auto peer = _controller->key().peer();
const auto topic = _controller->key().topic();
const auto sublist = _controller->key().sublist();
const auto topicRootId = topic ? topic->rootId() : MsgId();
const auto monoforumPeerId = sublist
? sublist->sublistPeer()->id
: PeerId();
const auto migrated = _controller->migrated();
const auto addMediaButton = [&](
Type buttonType,
const style::icon &icon) {
if (buttonType == type()) {
return;
}
auto result = AddButton(
content,
_controller,
peer,
topicRootId,
monoforumPeerId,
migrated,
buttonType,
tracker);
object_ptr<Profile::FloatingIcon>(
result,
icon,
st::infoSharedMediaButtonIconPosition)->show();
};
addMediaButton(Type::Photo, st::infoIconMediaPhoto);
addMediaButton(Type::Video, st::infoIconMediaVideo);
addMediaButton(Type::File, st::infoIconMediaFile);
addMediaButton(Type::MusicFile, st::infoIconMediaAudio);
addMediaButton(Type::Link, st::infoIconMediaLink);
addMediaButton(Type::RoundVoiceFile, st::infoIconMediaVoice);
addMediaButton(Type::GIF, st::infoIconMediaGif);
content->add(object_ptr<Ui::FixedHeightWidget>(
content,
st::infoProfileSkip));
wrap->toggleOn(tracker.atLeastOneShownValue());
wrap->finishAnimating();
}
Type InnerWidget::type() const {
return _controller->section().mediaType();
}
void InnerWidget::visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) {
setChildVisibleTopBottom(_list, visibleTop, visibleBottom);
}
bool InnerWidget::showInternal(not_null<Memento*> memento) {
if (!_controller->validateMementoPeer(memento)) {
return false;
}
auto mementoType = memento->section().mediaType();
if (mementoType == type()) {
restoreState(memento);
return true;
}
return false;
}
object_ptr<ListWidget> InnerWidget::setupList() {
auto result = object_ptr<ListWidget>(this, _controller);
result->heightValue(
) | rpl::on_next(
[this] { refreshHeight(); },
result->lifetime());
using namespace rpl::mappers;
result->scrollToRequests(
) | rpl::map([widget = result.data()](int to) {
return Ui::ScrollToRequest {
widget->y() + to,
-1
};
}) | rpl::start_to_stream(
_scrollToRequests,
result->lifetime());
_selectedLists.fire(result->selectedListValue());
_listTops.fire(result->topValue());
_empty->setType(_controller->section().mediaType());
_controller->mediaSourceQueryValue(
) | rpl::on_next([this](const QString &query) {
_empty->setSearchQuery(query);
}, result->lifetime());
return result;
}
void InnerWidget::saveState(not_null<Memento*> memento) {
_list->saveState(memento);
}
void InnerWidget::restoreState(not_null<Memento*> memento) {
_list->restoreState(memento);
}
rpl::producer<SelectedItems> InnerWidget::selectedListValue() const {
return _selectedLists.events_starting_with(
_list->selectedListValue()
) | rpl::flatten_latest();
}
void InnerWidget::selectionAction(SelectionAction action) {
_list->selectionAction(action);
}
InnerWidget::~InnerWidget() = default;
int InnerWidget::resizeGetHeight(int newWidth) {
_inResize = true;
auto guard = gsl::finally([this] { _inResize = false; });
if (_otherTypes) {
_otherTypes->resizeToWidth(newWidth);
}
_list->resizeToWidth(newWidth);
_empty->resizeToWidth(newWidth);
return recountHeight();
}
void InnerWidget::refreshHeight() {
if (_inResize) {
return;
}
resize(width(), recountHeight());
}
int InnerWidget::recountHeight() {
auto top = 0;
if (_otherTypes) {
_otherTypes->moveToLeft(0, top);
top += _otherTypes->heightNoMargins() - st::lineWidth;
}
auto listHeight = 0;
if (_list) {
_list->moveToLeft(0, top);
listHeight = _list->heightNoMargins();
top += listHeight;
}
if (listHeight > 0) {
_empty->hide();
} else {
_empty->show();
_empty->moveToLeft(0, top);
top += _empty->heightNoMargins();
}
return top;
}
void InnerWidget::setScrollHeightValue(rpl::producer<int> value) {
using namespace rpl::mappers;
_empty->setFullHeight(rpl::combine(
std::move(value),
_listTops.events_starting_with(
_list->topValue()
) | rpl::flatten_latest(),
_1 - _2));
}
rpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {
return _scrollToRequests.events();
}
} // namespace Media
} // namespace Info

View File

@@ -0,0 +1,88 @@
/*
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/rp_widget.h"
#include "ui/widgets/scroll_area.h"
#include "base/unique_qptr.h"
#include "info/media/info_media_widget.h"
#include "info/media/info_media_list_widget.h"
namespace Ui {
class VerticalLayout;
class SearchFieldController;
} // namespace Ui
namespace Info {
class Controller;
} // namespace Info
namespace Info::Media {
class Memento;
class ListWidget;
class EmptyWidget;
class InnerWidget final : public Ui::RpWidget {
public:
InnerWidget(
QWidget *parent,
not_null<Controller*> controller);
bool showInternal(not_null<Memento*> memento);
void setIsStackBottom(bool isStackBottom) {
_isStackBottom = isStackBottom;
setupOtherTypes();
}
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
void setScrollHeightValue(rpl::producer<int> value);
rpl::producer<Ui::ScrollToRequest> scrollToRequests() const;
rpl::producer<SelectedItems> selectedListValue() const;
void selectionAction(SelectionAction action);
~InnerWidget();
protected:
int resizeGetHeight(int newWidth) override;
void visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) override;
private:
int recountHeight();
void refreshHeight();
// Allows showing additional shared media links and tabs.
// Used for shared media in Saved Messages.
void setupOtherTypes();
void createOtherTypes();
void createTypeButtons();
Type type() const;
object_ptr<ListWidget> setupList();
const not_null<Controller*> _controller;
object_ptr<Ui::VerticalLayout> _otherTypes = { nullptr };
object_ptr<ListWidget> _list = { nullptr };
object_ptr<EmptyWidget> _empty;
bool _inResize = false;
bool _isStackBottom = false;
rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
rpl::event_stream<rpl::producer<SelectedItems>> _selectedLists;
rpl::event_stream<rpl::producer<int>> _listTops;
};
} // namespace Info::Media

View File

@@ -0,0 +1,462 @@
/*
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 "info/media/info_media_list_section.h"
#include "storage/storage_shared_media.h"
#include "layout/layout_selection.h"
#include "ui/rect.h"
#include "ui/painter.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_info.h"
namespace Info::Media {
namespace {
constexpr auto kFloatingHeaderAlpha = 0.9;
} // namespace
ListSection::ListSection(Type type, not_null<ListSectionDelegate*> delegate)
: _type(type)
, _delegate(delegate)
, _hasFloatingHeader(delegate->sectionHasFloatingHeader())
, _mosaic(st::emojiPanWidth - st::inlineResultsLeft) {
}
bool ListSection::empty() const {
return _items.empty();
}
UniversalMsgId ListSection::minId() const {
Expects(!empty());
return GetUniversalId(_items.back()->getItem());
}
void ListSection::setTop(int top) {
_top = top;
}
int ListSection::top() const {
return _top;
}
void ListSection::setCanReorder(bool value) {
_canReorder = value;
}
int ListSection::height() const {
return _height;
}
int ListSection::bottom() const {
return top() + height();
}
bool ListSection::isOneColumn() const {
return _itemsInRow == 1;
}
bool ListSection::addItem(not_null<BaseLayout*> item) {
if (_items.empty() || belongsHere(item)) {
if (_items.empty()) {
setHeader(item);
}
appendItem(item);
return true;
}
return false;
}
void ListSection::finishSection() {
if (_type == Type::GIF) {
_mosaic.setPadding({
st::infoMediaSkip,
headerHeight(),
st::infoMediaSkip,
st::stickerPanPadding,
});
_mosaic.setRightSkip(st::infoMediaSkip);
_mosaic.addItems(_items);
}
}
void ListSection::setHeader(not_null<BaseLayout*> item) {
_header.setText(st::infoMediaHeaderStyle, _delegate->sectionTitle(item));
}
bool ListSection::belongsHere(
not_null<BaseLayout*> item) const {
Expects(!_items.empty());
return _delegate->sectionItemBelongsHere(item, _items.back());
}
void ListSection::appendItem(not_null<BaseLayout*> item) {
_items.push_back(item);
_byItem.emplace(item->getItem(), item);
}
bool ListSection::removeItem(not_null<const HistoryItem*> item) {
if (const auto i = _byItem.find(item); i != end(_byItem)) {
_items.erase(ranges::remove(_items, i->second), end(_items));
_byItem.erase(i);
refreshHeight();
return true;
}
return false;
}
void ListSection::reorderItems(int oldPosition, int newPosition) {
base::reorder(_items, oldPosition, newPosition);
refreshHeight();
}
QRect ListSection::findItemRect(
not_null<const BaseLayout*> item) const {
const auto position = item->position();
if (!_mosaic.empty()) {
return _mosaic.findRect(position);
}
const auto top = position / _itemsInRow;
const auto indexInRow = position % _itemsInRow;
const auto left = _itemsLeft
+ indexInRow * (_itemWidth + st::infoMediaSkip);
return QRect(left, top, _itemWidth, item->height());
}
ListFoundItem ListSection::completeResult(
not_null<BaseLayout*> item,
bool exact) const {
return { item, findItemRect(item), exact };
}
ListFoundItem ListSection::findItemByPoint(QPoint point) const {
Expects(!_items.empty());
if (!_mosaic.empty()) {
const auto found = _mosaic.findByPoint(point);
Assert(found.index != -1);
const auto item = _mosaic.itemAt(found.index);
const auto rect = findItemRect(item);
return { item, rect, found.exact };
}
auto itemIt = findItemAfterTop(point.y());
if (itemIt == end(_items)) {
--itemIt;
}
auto item = *itemIt;
auto rect = findItemRect(item);
if (point.y() >= rect.top()) {
auto shift = floorclamp(
point.x(),
(_itemWidth + st::infoMediaSkip),
0,
_itemsInRow);
while (shift-- && itemIt != _items.end()) {
++itemIt;
}
if (itemIt == _items.end()) {
--itemIt;
}
item = *itemIt;
rect = findItemRect(item);
}
return { item, rect, rect.contains(point) };
}
std::optional<ListFoundItem> ListSection::findItemByItem(
not_null<const HistoryItem*> item) const {
const auto i = _byItem.find(item);
if (i != end(_byItem)) {
return ListFoundItem{ i->second, findItemRect(i->second), true };
}
return std::nullopt;
}
ListFoundItem ListSection::findItemDetails(
not_null<BaseLayout*> item) const {
return { item, findItemRect(item), true };
}
auto ListSection::findItemAfterTop(
int top) -> Items::iterator {
Expects(_mosaic.empty());
return ranges::lower_bound(
_items,
top,
std::less_equal<>(),
[this](const auto &item) {
const auto itemTop = item->position() / _itemsInRow;
return itemTop + item->height();
});
}
auto ListSection::findItemAfterTop(
int top) const -> Items::const_iterator {
Expects(_mosaic.empty());
return ranges::lower_bound(
_items,
top,
std::less_equal<>(),
[this](const auto &item) {
const auto itemTop = item->position() / _itemsInRow;
return itemTop + item->height();
});
}
auto ListSection::findItemAfterBottom(
Items::const_iterator from,
int bottom) const -> Items::const_iterator {
Expects(_mosaic.empty());
return ranges::lower_bound(
from,
_items.end(),
bottom,
std::less<>(),
[this](const auto &item) {
const auto itemTop = item->position() / _itemsInRow;
return itemTop;
});
}
const ListSection::Items &ListSection::items() const {
return _items;
}
void ListSection::paint(
Painter &p,
const ListContext &context,
QRect clip,
int outerWidth) const {
const auto header = headerHeight();
if (QRect(0, 0, outerWidth, header).intersects(clip)) {
p.setPen(st::infoMediaHeaderFg);
_header.drawLeftElided(
p,
st::infoMediaHeaderPosition.x(),
st::infoMediaHeaderPosition.y(),
outerWidth - 2 * st::infoMediaHeaderPosition.x(),
outerWidth);
}
auto localContext = context.layoutContext;
if (!_mosaic.empty()) {
const auto paintItem = [&](not_null<BaseLayout*> item, QPoint point) {
p.translate(point.x(), point.y());
item->paint(
p,
clip.translated(-point),
itemSelection(item, context),
&localContext);
p.translate(-point.x(), -point.y());
};
_mosaic.paint(std::move(paintItem), clip);
return;
}
const auto fromIt = findItemAfterTop(clip.y());
const auto tillIt = findItemAfterBottom(
fromIt,
clip.y() + clip.height());
for (auto it = fromIt; it != tillIt; ++it) {
const auto item = *it;
if (item == context.draggedItem) {
continue;
}
auto rect = findItemRect(item);
rect.translate(item->shift());
localContext.skipBorder = (rect.y() <= header + _itemsTop);
if (rect.intersects(clip)) {
p.translate(rect.topLeft());
item->paint(
p,
clip.translated(-rect.topLeft()),
itemSelection(item, context),
&localContext);
p.translate(-rect.topLeft());
if (_canReorder && isOneColumn()) {
st::stickersReorderIcon.paint(
p,
rect::right(rect) - oneColumnRightPadding(),
(rect.height() - st::stickersReorderIcon.height()) / 2
+ rect.y(),
outerWidth);
}
}
}
}
void ListSection::paintFloatingHeader(
Painter &p,
int visibleTop,
int outerWidth) {
if (!_hasFloatingHeader) {
return;
}
const auto headerTop = st::infoMediaHeaderPosition.y() / 2;
if (visibleTop <= (_top + headerTop)) {
return;
}
const auto header = headerHeight();
const auto headerLeft = st::infoMediaHeaderPosition.x();
const auto floatingTop = std::min(
visibleTop,
bottom() - header + headerTop);
p.save();
p.resetTransform();
p.setOpacity(kFloatingHeaderAlpha);
p.fillRect(QRect(0, floatingTop, outerWidth, header), st::boxBg);
p.setOpacity(1.0);
p.setPen(st::infoMediaHeaderFg);
_header.drawLeftElided(
p,
headerLeft,
floatingTop + headerTop,
outerWidth - 2 * headerLeft,
outerWidth);
p.restore();
}
TextSelection ListSection::itemSelection(
not_null<const BaseLayout*> item,
const ListContext &context) const {
const auto parent = item->getItem();
const auto dragSelectAction = context.dragSelectAction;
if (dragSelectAction != ListDragSelectAction::None) {
const auto i = context.dragSelected->find(parent);
if (i != context.dragSelected->end()) {
return (dragSelectAction == ListDragSelectAction::Selecting)
? FullSelection
: TextSelection();
}
}
const auto i = context.selected->find(parent);
return (i == context.selected->cend())
? TextSelection()
: i->second.text;
}
int ListSection::headerHeight() const {
return _header.isEmpty() ? 0 : st::infoMediaHeaderHeight;
}
int ListSection::oneColumnRightPadding() const {
return !isOneColumn()
? 0
: _canReorder
? st::stickersReorderIcon.width() + st::infoMediaLeft
: 0;
}
void ListSection::resizeToWidth(int newWidth) {
const auto minWidth = st::infoMediaMinGridSize + st::infoMediaSkip * 2;
if (newWidth < minWidth) {
return;
}
const auto resizeOneColumn = [&](int itemsLeft, int itemWidth) {
const auto rightPadding = oneColumnRightPadding();
_itemsLeft = itemsLeft;
_itemsTop = 0;
_itemsInRow = 1;
_itemWidth = itemWidth - rightPadding;
for (auto &item : _items) {
item->resizeGetHeight(_itemWidth - rightPadding);
}
};
switch (_type) {
case Type::Photo:
case Type::Video:
case Type::PhotoVideo:
case Type::RoundFile: {
const auto skip = st::infoMediaSkip;
_itemsLeft = st::infoMediaLeft;
_itemsTop = st::infoMediaSkip;
_itemsInRow = (newWidth - _itemsLeft * 2 + skip)
/ (st::infoMediaMinGridSize + skip);
_itemWidth = ((newWidth - _itemsLeft * 2 + skip) / _itemsInRow)
- st::infoMediaSkip;
_itemsLeft = (newWidth - (_itemWidth + skip) * _itemsInRow + skip)
/ 2;
for (auto &item : _items) {
_itemHeight = item->resizeGetHeight(_itemWidth);
}
} break;
case Type::GIF: {
_mosaic.setFullWidth(newWidth - st::infoMediaSkip);
} break;
case Type::RoundVoiceFile:
case Type::MusicFile:
resizeOneColumn(0, newWidth);
break;
case Type::File:
case Type::Link: {
const auto itemsLeft = st::infoMediaHeaderPosition.x();
const auto itemWidth = newWidth - 2 * itemsLeft;
resizeOneColumn(itemsLeft, itemWidth);
} break;
}
refreshHeight();
}
int ListSection::recountHeight() {
auto result = headerHeight();
switch (_type) {
case Type::Photo:
case Type::Video:
case Type::PhotoVideo:
case Type::RoundFile: {
const auto itemHeight = _itemHeight + st::infoMediaSkip;
auto index = 0;
result += _itemsTop;
for (auto &item : _items) {
item->setPosition(_itemsInRow * result + index);
if (++index == _itemsInRow) {
result += itemHeight;
index = 0;
}
}
if (_items.size() % _itemsInRow) {
_rowsCount = int(_items.size()) / _itemsInRow + 1;
result += itemHeight;
} else {
_rowsCount = int(_items.size()) / _itemsInRow;
}
} break;
case Type::GIF: {
return _mosaic.countDesiredHeight(0);
} break;
case Type::RoundVoiceFile:
case Type::File:
case Type::MusicFile:
case Type::Link:
for (auto &item : _items) {
item->setPosition(result);
result += item->height();
}
_rowsCount = _items.size();
break;
}
return result;
}
void ListSection::refreshHeight() {
_height = recountHeight();
}
} // namespace Info::Media

View File

@@ -0,0 +1,100 @@
/*
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 "info/media/info_media_common.h"
#include "layout/layout_mosaic.h"
#include "ui/text/text.h"
namespace Info::Media {
class ListSection {
public:
ListSection(Type type, not_null<ListSectionDelegate*> delegate);
bool addItem(not_null<BaseLayout*> item);
void finishSection();
[[nodiscard]] bool empty() const;
[[nodiscard]] UniversalMsgId minId() const;
void setTop(int top);
[[nodiscard]] int top() const;
void setCanReorder(bool);
void resizeToWidth(int newWidth);
[[nodiscard]] int height() const;
[[nodiscard]] int bottom() const;
[[nodiscard]] bool isOneColumn() const;
[[nodiscard]] int oneColumnRightPadding() const;
bool removeItem(not_null<const HistoryItem*> item);
void reorderItems(int oldPosition, int newPosition);
[[nodiscard]] std::optional<ListFoundItem> findItemByItem(
not_null<const HistoryItem*> item) const;
[[nodiscard]] ListFoundItem findItemDetails(
not_null<BaseLayout*> item) const;
[[nodiscard]] ListFoundItem findItemByPoint(QPoint point) const;
using Items = std::vector<not_null<BaseLayout*>>;
const Items &items() const;
void paint(
Painter &p,
const ListContext &context,
QRect clip,
int outerWidth) const;
void paintFloatingHeader(Painter &p, int visibleTop, int outerWidth);
private:
[[nodiscard]] int headerHeight() const;
void appendItem(not_null<BaseLayout*> item);
void setHeader(not_null<BaseLayout*> item);
[[nodiscard]] bool belongsHere(not_null<BaseLayout*> item) const;
[[nodiscard]] Items::iterator findItemAfterTop(int top);
[[nodiscard]] Items::const_iterator findItemAfterTop(int top) const;
[[nodiscard]] Items::const_iterator findItemAfterBottom(
Items::const_iterator from,
int bottom) const;
[[nodiscard]] QRect findItemRect(not_null<const BaseLayout*> item) const;
[[nodiscard]] ListFoundItem completeResult(
not_null<BaseLayout*> item,
bool exact) const;
[[nodiscard]] TextSelection itemSelection(
not_null<const BaseLayout*> item,
const ListContext &context) const;
int recountHeight();
void refreshHeight();
Type _type = Type{};
not_null<ListSectionDelegate*> _delegate;
bool _hasFloatingHeader = false;
Ui::Text::String _header;
Items _items;
base::flat_map<
not_null<const HistoryItem*>,
not_null<BaseLayout*>> _byItem;
int _itemsLeft = 0;
int _itemsTop = 0;
int _itemWidth = 0;
int _itemHeight = 0;
int _itemsInRow = 1;
mutable int _rowsCount = 0;
int _top = 0;
int _height = 0;
bool _canReorder = false;
Mosaic::Layout::MosaicLayout<BaseLayout> _mosaic;
};
} // namespace Info::Media

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,382 @@
/*
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/rp_widget.h"
#include "ui/widgets/tooltip.h"
#include "info/media/info_media_widget.h"
#include "info/media/info_media_common.h"
#include "overview/overview_layout_delegate.h"
class DeleteMessagesBox;
namespace Main {
class Session;
} // namespace Main
namespace HistoryView {
struct TextState;
struct StateRequest;
enum class CursorState : char;
enum class PointState : char;
} // namespace HistoryView
namespace Ui {
class PopupMenu;
class BoxContent;
} // namespace Ui
namespace Overview {
namespace Layout {
class ItemBase;
} // namespace Layout
} // namespace Overview
namespace Window {
class SessionController;
} // namespace Window
namespace Info {
class AbstractController;
namespace Media {
struct ListFoundItem;
struct ListFoundItemWithSection;
struct ListContext;
class ListSection;
class ListProvider;
class ListWidget final
: public Ui::RpWidget
, public Overview::Layout::Delegate
, public Ui::AbstractTooltipShower {
public:
ListWidget(
QWidget *parent,
not_null<AbstractController*> controller);
~ListWidget();
Main::Session &session() const;
void restart();
rpl::producer<int> scrollToRequests() const;
rpl::producer<SelectedItems> selectedListValue() const;
void selectionAction(SelectionAction action);
struct ReorderDescriptor {
Fn<void(int old, int pos, Fn<void()> done, Fn<void()> fail)> save;
Fn<bool(HistoryItem*)> filter;
};
void setReorderDescriptor(ReorderDescriptor descriptor);
QRect getCurrentSongGeometry();
rpl::producer<> checkForHide() const {
return _checkForHide.events();
}
bool preventAutoHide() const;
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
// Overview::Layout::Delegate
void registerHeavyItem(not_null<const BaseLayout*> item) override;
void unregisterHeavyItem(not_null<const BaseLayout*> item) override;
void repaintItem(not_null<const BaseLayout*> item) override;
bool itemVisible(not_null<const BaseLayout*> item) override;
not_null<StickerPremiumMark*> hiddenMark() override;
// AbstractTooltipShower interface
QString tooltipText() const override;
QPoint tooltipPos() const override;
bool tooltipWindowActive() const override;
void openPhoto(not_null<PhotoData*> photo, FullMsgId id) override;
void openDocument(
not_null<DocumentData*> document,
FullMsgId id,
bool showInMediaView = false) override;
private:
struct DateBadge;
using Section = ListSection;
using FoundItem = ListFoundItem;
using CursorState = HistoryView::CursorState;
using TextState = HistoryView::TextState;
using StateRequest = HistoryView::StateRequest;
using SelectionData = ListItemSelectionData;
using SelectedMap = ListSelectedMap;
using DragSelectAction = ListDragSelectAction;
enum class MouseAction {
None,
PrepareDrag,
Dragging,
PrepareSelect,
Selecting,
PrepareReorder,
Reordering,
};
struct ReorderState {
bool enabled = false;
int index = -1;
int targetIndex = -1;
QPoint startPos;
QPoint dragPoint;
QPoint currentPos;
BaseLayout *item = nullptr;
const Section *section = nullptr;
};
struct ShiftAnimation {
Ui::Animations::Simple xAnimation;
Ui::Animations::Simple yAnimation;
int shift = 0;
int targetShift = 0;
};
struct MouseState {
HistoryItem *item = nullptr;
QSize size;
QPoint cursor;
bool inside = false;
inline bool operator==(const MouseState &other) const {
return (item == other.item)
&& (cursor == other.cursor);
}
inline bool operator!=(const MouseState &other) const {
return !(*this == other);
}
};
enum class ContextMenuSource {
Mouse,
Touch,
Other,
};
int resizeGetHeight(int newWidth) override;
void visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) override;
void paintEvent(QPaintEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void mouseDoubleClickEvent(QMouseEvent *e) override;
void contextMenuEvent(QContextMenuEvent *e) override;
void enterEventHook(QEnterEvent *e) override;
void leaveEventHook(QEvent *e) override;
void start();
int recountHeight();
void refreshHeight();
void subscribeToSession(
not_null<Main::Session*> session,
rpl::lifetime &lifetime);
void setupSelectRestriction();
[[nodiscard]] MsgId topicRootId() const;
[[nodiscard]] PeerId monoforumPeerId() const;
QMargins padding() const;
bool isItemLayout(
not_null<const HistoryItem*> item,
BaseLayout *layout) const;
void repaintItem(const HistoryItem *item);
void repaintItem(const BaseLayout *item);
void repaintItem(QRect itemGeometry);
void itemRemoved(not_null<const HistoryItem*> item);
void itemLayoutChanged(not_null<const HistoryItem*> item);
void refreshRows();
void markStoryMsgsSelected();
void trackSession(not_null<Main::Session*> session);
[[nodiscard]] SelectedItems collectSelectedItems() const;
[[nodiscard]] MessageIdsList collectSelectedIds() const;
[[nodiscard]] MessageIdsList collectSelectedIds(
const SelectedItems &items) const;
void pushSelectedItems();
[[nodiscard]] bool hasSelected() const;
[[nodiscard]] bool isSelectedItem(
const SelectedMap::const_iterator &i) const;
void removeItemSelection(
const SelectedMap::const_iterator &i);
[[nodiscard]] bool hasSelectedText() const;
[[nodiscard]] bool hasSelectedItems() const;
void clearSelected();
void forwardSelected();
void forwardItem(GlobalMsgId globalId);
void forwardItems(MessageIdsList &&items);
void deleteSelected();
void toggleStoryPinSelected();
void toggleStoryInProfileSelected(bool toProfile);
void deleteItem(GlobalMsgId globalId);
void deleteItems(SelectedItems &&items, Fn<void()> confirmed = nullptr);
void toggleStoryInProfile(
MessageIdsList &&items,
bool toProfile,
Fn<void()> confirmed = nullptr);
void toggleStoryPin(
MessageIdsList &&items,
bool pin,
Fn<void()> confirmed = nullptr);
void applyItemSelection(
HistoryItem *item,
TextSelection selection);
void toggleItemSelection(not_null<HistoryItem*> item);
[[nodiscard]] SelectedMap::iterator itemUnderPressSelection();
[[nodiscard]] auto itemUnderPressSelection() const
-> SelectedMap::const_iterator;
bool isItemUnderPressSelected() const;
[[nodiscard]] bool requiredToStartDragging(
not_null<BaseLayout*> layout) const;
[[nodiscard]] bool isPressInSelectedText(TextState state) const;
void applyDragSelection();
void applyDragSelection(SelectedMap &applyTo) const;
[[nodiscard]] bool isAfter(
const MouseState &a,
const MouseState &b) const;
[[nodiscard]] static bool SkipSelectFromItem(const MouseState &state);
[[nodiscard]] static bool SkipSelectTillItem(const MouseState &state);
[[nodiscard]] std::vector<Section>::iterator findSectionByItem(
not_null<const HistoryItem*> item);
[[nodiscard]] std::vector<Section>::iterator findSectionAfterTop(
int top);
[[nodiscard]] std::vector<Section>::const_iterator findSectionAfterTop(
int top) const;
[[nodiscard]] auto findSectionAfterBottom(
std::vector<Section>::const_iterator from,
int bottom) const -> std::vector<Section>::const_iterator;
[[nodiscard]] auto findSectionAndItem(QPoint point) const
-> std::pair<std::vector<Section>::const_iterator, FoundItem>;
[[nodiscard]] FoundItem findItemByPoint(QPoint point) const;
[[nodiscard]] ListFoundItemWithSection findItemByPointWithSection(QPoint point) const;
[[nodiscard]] std::optional<FoundItem> findItemByItem(
const HistoryItem *item);
[[nodiscard]] FoundItem findItemDetails(not_null<BaseLayout*> item);
[[nodiscard]] FoundItem foundItemInSection(
const FoundItem &item,
const Section &section) const;
[[nodiscard]] ListScrollTopState countScrollState() const;
void saveScrollState();
void restoreScrollState();
[[nodiscard]] QPoint clampMousePosition(QPoint position) const;
void mouseActionStart(
const QPoint &globalPosition,
Qt::MouseButton button);
void mouseActionUpdate(const QPoint &globalPosition);
void mouseActionUpdate();
void mouseActionFinish(
const QPoint &globalPosition,
Qt::MouseButton button);
void mouseActionCancel();
void performDrag();
[[nodiscard]] style::cursor computeMouseCursor() const;
void showContextMenu(
QContextMenuEvent *e,
ContextMenuSource source);
void updateDragSelection();
void clearDragSelection();
void updateDateBadgeFor(int top);
void scrollDateCheck();
void scrollDateHide();
void toggleScrollDateShown();
void trySwitchToWordSelection();
void switchToWordSelection();
void validateTrippleClickStartTime();
void checkMoveToOtherViewer();
void clearHeavyItems();
void setActionBoxWeak(base::weak_qptr<Ui::BoxContent> box);
void setupStoriesTrackIds();
void startReorder(const QPoint &globalPos);
void updateReorder(const QPoint &globalPos);
void finishReorder();
void cancelReorder();
void updateShiftAnimations();
[[nodiscard]] int itemIndexFromPoint(QPoint point) const;
[[nodiscard]] QRect itemGeometryByIndex(int index);
[[nodiscard]] BaseLayout *itemByIndex(int index);
[[nodiscard]] bool canReorder() const;
void reorderItemsInSections(int oldIndex, int newIndex);
void resetAllItemShifts();
void finishShiftAnimations();
const not_null<AbstractController*> _controller;
const std::unique_ptr<ListProvider> _provider;
base::flat_set<not_null<const BaseLayout*>> _heavyLayouts;
bool _heavyLayoutsInvalidated = false;
std::vector<Section> _sections;
int _visibleTop = 0;
int _visibleBottom = 0;
ListScrollTopState _scrollTopState;
rpl::event_stream<int> _scrollToRequests;
MouseAction _mouseAction = MouseAction::None;
TextSelectType _mouseSelectType = TextSelectType::Letters;
QPoint _mousePosition;
MouseState _overState;
MouseState _pressState;
BaseLayout *_overLayout = nullptr;
HistoryItem *_contextItem = nullptr;
CursorState _mouseCursorState = CursorState();
uint16 _mouseTextSymbol = 0;
bool _pressWasInactive = false;
SelectedMap _selected;
SelectedMap _dragSelected;
rpl::event_stream<SelectedItems> _selectedListStream;
style::cursor _cursor = style::cur_default;
DragSelectAction _dragSelectAction = DragSelectAction::None;
bool _wasSelectedText = false; // was some text selected in current drag action
const std::unique_ptr<DateBadge> _dateBadge;
int _selectedLimit = 0;
int _storiesAddToAlbumId = 0;
int _storiesAddToAlbumTotal = 0;
base::flat_set<StoryId> _storiesInAlbum;
base::flat_set<MsgId> _storyMsgsToMarkSelected;
std::unique_ptr<StickerPremiumMark> _hiddenMark;
base::unique_qptr<Ui::PopupMenu> _contextMenu;
rpl::event_stream<> _checkForHide;
base::weak_qptr<Ui::BoxContent> _actionBoxWeak;
rpl::lifetime _actionBoxWeakLifetime;
QPoint _trippleClickPoint;
crl::time _trippleClickStartTime = 0;
base::flat_map<not_null<Main::Session*>, rpl::lifetime> _trackedSessions;
ReorderState _reorderState;
base::flat_map<int, ShiftAnimation> _shiftAnimations;
int _activeShiftAnimations = 0;
Ui::Animations::Simple _returnAnimation;
ReorderDescriptor _reorderDescriptor;
bool _inDragArea = false;
};
} // namespace Media
} // namespace Info

View File

@@ -0,0 +1,578 @@
/*
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 "info/media/info_media_provider.h"
#include "info/media/info_media_widget.h"
#include "info/media/info_media_list_section.h"
#include "info/info_controller.h"
#include "layout/layout_selection.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "lang/lang_keys.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_helpers.h"
#include "data/data_session.h"
#include "data/data_chat.h"
#include "data/data_channel.h"
#include "data/data_forum_topic.h"
#include "data/data_user.h"
#include "data/data_peer_values.h"
#include "data/data_document.h"
#include "data/data_saved_sublist.h"
#include "styles/style_info.h"
#include "styles/style_overview.h"
namespace Info::Media {
namespace {
constexpr auto kPreloadedScreensCount = 4;
constexpr auto kPreloadedScreensCountFull
= kPreloadedScreensCount + 1 + kPreloadedScreensCount;
} // namespace
Provider::Provider(not_null<AbstractController*> controller)
: _controller(controller)
, _peer(_controller->key().peer())
, _topicRootId(_controller->key().topic()
? _controller->key().topic()->rootId()
: MsgId())
, _monoforumPeerId(_controller->key().sublist()
? _controller->key().sublist()->sublistPeer()->id
: PeerId())
, _migrated(_controller->migrated())
, _type(_controller->section().mediaType())
, _slice(sliceKey(_universalAroundId)) {
_controller->session().data().itemRemoved(
) | rpl::on_next([this](auto item) {
itemRemoved(item);
}, _lifetime);
style::PaletteChanged(
) | rpl::on_next([=] {
for (auto &layout : _layouts) {
layout.second.item->invalidateCache();
}
}, _lifetime);
_controller->session().appConfig().ignoredRestrictionReasonsChanges(
) | rpl::on_next([=](std::vector<QString> &&changed) {
const auto sensitive = Data::UnavailableReason::Sensitive();
if (ranges::contains(changed, sensitive.reason)) {
for (auto &[id, layout] : _layouts) {
layout.item->maybeClearSensitiveSpoiler();
}
}
}, _lifetime);
}
Type Provider::type() {
return _type;
}
bool Provider::hasSelectRestriction() {
if (_peer->session().frozen()) {
return true;
} else if (_peer->allowsForwarding()) {
return false;
} else if (const auto chat = _peer->asChat()) {
return !chat->canDeleteMessages();
} else if (const auto channel = _peer->asChannel()) {
return !channel->canDeleteMessages();
}
return true;
}
rpl::producer<bool> Provider::hasSelectRestrictionChanges() {
if (_peer->isUser()) {
return rpl::never<bool>();
}
const auto chat = _peer->asChat();
const auto channel = _peer->asChannel();
auto noForwards = chat
? Data::PeerFlagValue(chat, ChatDataFlag::NoForwards)
: Data::PeerFlagValue(
channel,
ChannelDataFlag::NoForwards
) | rpl::type_erased;
auto rights = chat
? chat->adminRightsValue()
: channel->adminRightsValue();
auto canDelete = std::move(
rights
) | rpl::map([=] {
return chat
? chat->canDeleteMessages()
: channel->canDeleteMessages();
});
return rpl::combine(
std::move(noForwards),
std::move(canDelete)
) | rpl::map([=] {
return hasSelectRestriction();
}) | rpl::distinct_until_changed() | rpl::skip(1);
}
bool Provider::sectionHasFloatingHeader() {
switch (_type) {
case Type::Photo:
case Type::GIF:
case Type::Video:
case Type::RoundFile:
case Type::RoundVoiceFile:
case Type::MusicFile:
return false;
case Type::File:
case Type::Link:
return true;
}
Unexpected("Type in HasFloatingHeader()");
}
QString Provider::sectionTitle(not_null<const BaseLayout*> item) {
switch (_type) {
case Type::Photo:
case Type::GIF:
case Type::Video:
case Type::RoundFile:
case Type::RoundVoiceFile:
case Type::File:
return langMonthFull(item->dateTime().date());
case Type::Link:
return langDayOfMonthFull(item->dateTime().date());
case Type::MusicFile:
return QString();
}
Unexpected("Type in ListSection::setHeader()");
}
bool Provider::sectionItemBelongsHere(
not_null<const BaseLayout*> item,
not_null<const BaseLayout*> previous) {
const auto date = item->dateTime().date();
const auto sectionDate = previous->dateTime().date();
switch (_type) {
case Type::Photo:
case Type::GIF:
case Type::Video:
case Type::RoundFile:
case Type::RoundVoiceFile:
case Type::File:
return date.year() == sectionDate.year()
&& date.month() == sectionDate.month();
case Type::Link:
return date == sectionDate;
case Type::MusicFile:
return true;
}
Unexpected("Type in ListSection::belongsHere()");
}
bool Provider::isPossiblyMyItem(not_null<const HistoryItem*> item) {
return isPossiblyMyPeerId(item->history()->peer->id);
}
bool Provider::isPossiblyMyPeerId(PeerId peerId) const {
return (peerId == _peer->id) || (_migrated && peerId == _migrated->id);
}
std::optional<int> Provider::fullCount() {
return _slice.fullCount();
}
void Provider::restart() {
_layouts.clear();
_universalAroundId = kDefaultAroundId;
_idsLimit = kMinimalIdsLimit;
_slice = SparseIdsMergedSlice(sliceKey(_universalAroundId));
refreshViewer();
}
void Provider::checkPreload(
QSize viewport,
not_null<BaseLayout*> topLayout,
not_null<BaseLayout*> bottomLayout,
bool preloadTop,
bool preloadBottom) {
const auto visibleWidth = viewport.width();
const auto visibleHeight = viewport.height();
const auto preloadedHeight = kPreloadedScreensCountFull * visibleHeight;
const auto minItemHeight = MinItemHeight(_type, visibleWidth);
const auto preloadedCount = preloadedHeight / minItemHeight;
const auto preloadIdsLimitMin = (preloadedCount / 2) + 1;
const auto preloadIdsLimit = preloadIdsLimitMin
+ (visibleHeight / minItemHeight);
const auto after = _slice.skippedAfter();
const auto topLoaded = after && (*after == 0);
const auto before = _slice.skippedBefore();
const auto bottomLoaded = before && (*before == 0);
const auto minScreenDelta = kPreloadedScreensCount
- kPreloadIfLessThanScreens;
const auto minUniversalIdDelta = (minScreenDelta * visibleHeight)
/ minItemHeight;
const auto preloadAroundItem = [&](not_null<BaseLayout*> layout) {
auto preloadRequired = false;
auto universalId = GetUniversalId(layout);
if (!preloadRequired) {
preloadRequired = (_idsLimit < preloadIdsLimitMin);
}
if (!preloadRequired) {
auto delta = _slice.distance(
sliceKey(_universalAroundId),
sliceKey(universalId));
Assert(delta != std::nullopt);
preloadRequired = (qAbs(*delta) >= minUniversalIdDelta);
}
if (preloadRequired) {
_idsLimit = preloadIdsLimit;
_universalAroundId = universalId;
refreshViewer();
}
};
if (preloadTop && !topLoaded) {
preloadAroundItem(topLayout);
} else if (preloadBottom && !bottomLoaded) {
preloadAroundItem(bottomLayout);
}
}
void Provider::refreshViewer() {
_viewerLifetime.destroy();
const auto idForViewer = sliceKey(_universalAroundId).universalId;
_controller->mediaSource(
idForViewer,
_idsLimit,
_idsLimit
) | rpl::on_next([=](SparseIdsMergedSlice &&slice) {
if (!slice.fullCount()) {
// Don't display anything while full count is unknown.
return;
}
_slice = std::move(slice);
if (auto nearest = _slice.nearest(idForViewer)) {
_universalAroundId = GetUniversalId(*nearest);
}
_refreshed.fire({});
}, _viewerLifetime);
}
rpl::producer<> Provider::refreshed() {
return _refreshed.events();
}
std::vector<ListSection> Provider::fillSections(
not_null<Overview::Layout::Delegate*> delegate) {
markLayoutsStale();
const auto guard = gsl::finally([&] { clearStaleLayouts(); });
auto result = std::vector<ListSection>();
auto section = ListSection(_type, sectionDelegate());
auto count = _slice.size();
for (auto i = count; i != 0;) {
auto universalId = GetUniversalId(_slice[--i]);
if (auto layout = getLayout(universalId, delegate)) {
if (!section.addItem(layout)) {
section.finishSection();
result.push_back(std::move(section));
section = ListSection(_type, sectionDelegate());
section.addItem(layout);
}
}
}
if (!section.empty()) {
section.finishSection();
result.push_back(std::move(section));
}
return result;
}
void Provider::markLayoutsStale() {
for (auto &layout : _layouts) {
layout.second.stale = true;
}
}
void Provider::clearStaleLayouts() {
for (auto i = _layouts.begin(); i != _layouts.end();) {
if (i->second.stale) {
_layoutRemoved.fire(i->second.item.get());
i = _layouts.erase(i);
} else {
++i;
}
}
}
rpl::producer<not_null<BaseLayout*>> Provider::layoutRemoved() {
return _layoutRemoved.events();
}
BaseLayout *Provider::lookupLayout(
const HistoryItem *item) {
const auto i = _layouts.find(GetUniversalId(item));
return (i != _layouts.end()) ? i->second.item.get() : nullptr;
}
bool Provider::isMyItem(not_null<const HistoryItem*> item) {
const auto peer = item->history()->peer;
return (_peer == peer) || (_migrated == peer);
}
bool Provider::isAfter(
not_null<const HistoryItem*> a,
not_null<const HistoryItem*> b) {
return (GetUniversalId(a) < GetUniversalId(b));
}
void Provider::setSearchQuery(QString query) {
Unexpected("Media::Provider::setSearchQuery.");
}
SparseIdsMergedSlice::Key Provider::sliceKey(
UniversalMsgId universalId) const {
using Key = SparseIdsMergedSlice::Key;
if (!_topicRootId && _migrated) {
return Key(
_peer->id,
_topicRootId,
_monoforumPeerId,
_migrated->id,
universalId);
}
if (universalId < 0) {
// Convert back to plain id for non-migrated histories.
universalId = universalId + ServerMaxMsgId;
}
return Key(
_peer->id,
_topicRootId,
_monoforumPeerId,
PeerId(),
universalId);
}
void Provider::itemRemoved(not_null<const HistoryItem*> item) {
const auto id = GetUniversalId(item);
if (const auto i = _layouts.find(id); i != end(_layouts)) {
_layoutRemoved.fire(i->second.item.get());
_layouts.erase(i);
}
}
FullMsgId Provider::computeFullId(
UniversalMsgId universalId) const {
Expects(universalId != 0);
return (universalId > 0)
? FullMsgId(_peer->id, universalId)
: FullMsgId(
(_migrated ? _migrated : _peer.get())->id,
ServerMaxMsgId + universalId);
}
BaseLayout *Provider::getLayout(
UniversalMsgId universalId,
not_null<Overview::Layout::Delegate*> delegate) {
auto it = _layouts.find(universalId);
if (it == _layouts.end()) {
if (auto layout = createLayout(universalId, delegate, _type)) {
layout->initDimensions();
it = _layouts.emplace(
universalId,
std::move(layout)).first;
} else {
return nullptr;
}
}
it->second.stale = false;
return it->second.item.get();
}
std::unique_ptr<BaseLayout> Provider::createLayout(
UniversalMsgId universalId,
not_null<Overview::Layout::Delegate*> delegate,
Type type) {
const auto item = _controller->session().data().message(
computeFullId(universalId));
if (!item) {
return nullptr;
}
const auto getPhoto = [&]() -> PhotoData* {
if (const auto media = item->media()) {
return media->photo();
}
return nullptr;
};
const auto getFile = [&]() -> DocumentData* {
if (const auto media = item->media()) {
return media->document();
}
return nullptr;
};
const auto &songSt = st::overviewFileLayout;
using namespace Overview::Layout;
const auto options = [&] {
const auto media = item->media();
return MediaOptions{ .spoiler = media && media->hasSpoiler() };
};
switch (type) {
case Type::Photo:
if (const auto photo = getPhoto()) {
return std::make_unique<Photo>(
delegate,
item,
photo,
options());
}
return nullptr;
case Type::GIF:
if (const auto file = getFile()) {
return std::make_unique<Gif>(delegate, item, file);
}
return nullptr;
case Type::Video:
if (const auto file = getFile()) {
return std::make_unique<Video>(delegate, item, file, options());
}
return nullptr;
case Type::File:
if (const auto file = getFile()) {
return std::make_unique<Document>(
delegate,
item,
DocumentFields{ .document = file },
songSt);
}
return nullptr;
case Type::MusicFile:
if (const auto file = getFile()) {
return std::make_unique<Document>(
delegate,
item,
DocumentFields{ .document = file },
songSt);
}
return nullptr;
case Type::RoundVoiceFile:
if (const auto file = getFile()) {
return std::make_unique<Voice>(delegate, item, file, songSt);
}
return nullptr;
case Type::Link:
return std::make_unique<Link>(delegate, item, item->media());
case Type::RoundFile:
return nullptr;
}
Unexpected("Type in ListWidget::createLayout()");
}
ListItemSelectionData Provider::computeSelectionData(
not_null<const HistoryItem*> item,
TextSelection selection) {
auto result = ListItemSelectionData(selection);
result.canDelete = item->canDelete();
result.canForward = item->allowsForward();
return result;
}
bool Provider::allowSaveFileAs(
not_null<const HistoryItem*> item,
not_null<DocumentData*> document) {
return item->allowsForward();
}
QString Provider::showInFolderPath(
not_null<const HistoryItem*> item,
not_null<DocumentData*> document) {
return document->filepath(true);
}
void Provider::applyDragSelection(
ListSelectedMap &selected,
not_null<const HistoryItem*> fromItem,
bool skipFrom,
not_null<const HistoryItem*> tillItem,
bool skipTill) {
const auto fromId = GetUniversalId(fromItem) - (skipFrom ? 1 : 0);
const auto tillId = GetUniversalId(tillItem) - (skipTill ? 0 : 1);
for (auto i = selected.begin(); i != selected.end();) {
const auto itemId = GetUniversalId(i->first);
if (itemId > fromId || itemId <= tillId) {
i = selected.erase(i);
} else {
++i;
}
}
for (auto &layoutItem : _layouts) {
auto &&universalId = layoutItem.first;
if (universalId <= fromId && universalId > tillId) {
const auto item = layoutItem.second.item->getItem();
ChangeItemSelection(
selected,
item,
computeSelectionData(item, FullSelection));
}
}
}
int64 Provider::scrollTopStatePosition(not_null<HistoryItem*> item) {
return GetUniversalId(item).bare;
}
HistoryItem *Provider::scrollTopStateItem(ListScrollTopState state) {
if (state.item && _slice.indexOf(state.item->fullId())) {
return state.item;
} else if (const auto id = _slice.nearest(state.position)) {
if (const auto item = _controller->session().data().message(*id)) {
return item;
}
}
return state.item;
}
void Provider::saveState(
not_null<Memento*> memento,
ListScrollTopState scrollState) {
if (_universalAroundId != kDefaultAroundId && scrollState.item) {
memento->setAroundId(computeFullId(_universalAroundId));
memento->setIdsLimit(_idsLimit);
memento->setScrollTopItem(scrollState.item->globalId());
memento->setScrollTopItemPosition(scrollState.position);
memento->setScrollTopShift(scrollState.shift);
}
}
void Provider::restoreState(
not_null<Memento*> memento,
Fn<void(ListScrollTopState)> restoreScrollState) {
if (const auto limit = memento->idsLimit()) {
auto wasAroundId = memento->aroundId();
if (isPossiblyMyPeerId(wasAroundId.peer)) {
_idsLimit = limit;
_universalAroundId = GetUniversalId(wasAroundId);
restoreScrollState({
.position = memento->scrollTopItemPosition(),
.item = MessageByGlobalId(memento->scrollTopItem()),
.shift = memento->scrollTopShift(),
});
refreshViewer();
}
}
}
} // namespace Info::Media

View File

@@ -0,0 +1,125 @@
/*
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 "info/media/info_media_common.h"
#include "data/data_shared_media.h"
namespace Info {
class AbstractController;
} // namespace Info
namespace Info::Media {
class Provider final : public ListProvider, private ListSectionDelegate {
public:
explicit Provider(not_null<AbstractController*> controller);
Type type() override;
bool hasSelectRestriction() override;
rpl::producer<bool> hasSelectRestrictionChanges() override;
bool isPossiblyMyItem(not_null<const HistoryItem*> item) override;
std::optional<int> fullCount() override;
void restart() override;
void checkPreload(
QSize viewport,
not_null<BaseLayout*> topLayout,
not_null<BaseLayout*> bottomLayout,
bool preloadTop,
bool preloadBottom) override;
void refreshViewer() override;
rpl::producer<> refreshed() override;
std::vector<ListSection> fillSections(
not_null<Overview::Layout::Delegate*> delegate) override;
rpl::producer<not_null<BaseLayout*>> layoutRemoved() override;
BaseLayout *lookupLayout(const HistoryItem *item) override;
bool isMyItem(not_null<const HistoryItem*> item) override;
bool isAfter(
not_null<const HistoryItem*> a,
not_null<const HistoryItem*> b) override;
void setSearchQuery(QString query) override;
ListItemSelectionData computeSelectionData(
not_null<const HistoryItem*> item,
TextSelection selection) override;
void applyDragSelection(
ListSelectedMap &selected,
not_null<const HistoryItem*> fromItem,
bool skipFrom,
not_null<const HistoryItem*> tillItem,
bool skipTill) override;
bool allowSaveFileAs(
not_null<const HistoryItem*> item,
not_null<DocumentData*> document) override;
QString showInFolderPath(
not_null<const HistoryItem*> item,
not_null<DocumentData*> document) override;
int64 scrollTopStatePosition(not_null<HistoryItem*> item) override;
HistoryItem *scrollTopStateItem(ListScrollTopState state) override;
void saveState(
not_null<Memento*> memento,
ListScrollTopState scrollState) override;
void restoreState(
not_null<Memento*> memento,
Fn<void(ListScrollTopState)> restoreScrollState) override;
private:
static constexpr auto kMinimalIdsLimit = 16;
static constexpr auto kDefaultAroundId = (ServerMaxMsgId - 1);
bool sectionHasFloatingHeader() override;
QString sectionTitle(not_null<const BaseLayout*> item) override;
bool sectionItemBelongsHere(
not_null<const BaseLayout*> item,
not_null<const BaseLayout*> previous) override;
[[nodiscard]] bool isPossiblyMyPeerId(PeerId peerId) const;
[[nodiscard]] FullMsgId computeFullId(UniversalMsgId universalId) const;
[[nodiscard]] BaseLayout *getLayout(
UniversalMsgId universalId,
not_null<Overview::Layout::Delegate*> delegate);
[[nodiscard]] std::unique_ptr<BaseLayout> createLayout(
UniversalMsgId universalId,
not_null<Overview::Layout::Delegate*> delegate,
Type type);
[[nodiscard]] SparseIdsMergedSlice::Key sliceKey(
UniversalMsgId universalId) const;
void itemRemoved(not_null<const HistoryItem*> item);
void markLayoutsStale();
void clearStaleLayouts();
const not_null<AbstractController*> _controller;
const not_null<PeerData*> _peer;
const MsgId _topicRootId = 0;
const PeerId _monoforumPeerId = 0;
PeerData * const _migrated = nullptr;
const Type _type = Type::Photo;
UniversalMsgId _universalAroundId = kDefaultAroundId;
int _idsLimit = kMinimalIdsLimit;
SparseIdsMergedSlice _slice;
std::unordered_map<UniversalMsgId, CachedItem> _layouts;
rpl::event_stream<not_null<BaseLayout*>> _layoutRemoved;
rpl::event_stream<> _refreshed;
rpl::lifetime _lifetime;
rpl::lifetime _viewerLifetime;
};
} // namespace Info::Media

View File

@@ -0,0 +1,201 @@
/*
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 "info/media/info_media_widget.h"
#include "history/history.h"
#include "info/media/info_media_inner_widget.h"
#include "info/info_controller.h"
#include "main/main_session.h"
#include "ui/widgets/scroll_area.h"
#include "ui/search_field_controller.h"
#include "ui/ui_utility.h"
#include "data/data_peer.h"
#include "data/data_user.h"
#include "data/data_channel.h"
#include "data/data_forum_topic.h"
#include "data/data_saved_sublist.h"
#include "lang/lang_keys.h"
#include "styles/style_info.h"
namespace Info::Media {
std::optional<int> TypeToTabIndex(Type type) {
switch (type) {
case Type::Photo: return 0;
case Type::Video: return 1;
case Type::File: return 2;
}
return std::nullopt;
}
Type TabIndexToType(int index) {
switch (index) {
case 0: return Type::Photo;
case 1: return Type::Video;
case 2: return Type::File;
}
Unexpected("Index in Info::Media::TabIndexToType()");
}
tr::phrase<> SharedMediaTitle(Type type) {
switch (type) {
case Type::Photo:
return tr::lng_media_type_photos;
case Type::GIF:
return tr::lng_media_type_gifs;
case Type::Video:
return tr::lng_media_type_videos;
case Type::MusicFile:
return tr::lng_media_type_songs;
case Type::File:
return tr::lng_media_type_files;
case Type::RoundVoiceFile:
return tr::lng_media_type_audios;
case Type::Link:
return tr::lng_media_type_links;
case Type::RoundFile:
return tr::lng_media_type_rounds;
}
Unexpected("Bad media type in Info::TitleValue()");
}
Memento::Memento(not_null<Controller*> controller)
: Memento(
(controller->peer()
? controller->peer()
: controller->storiesPeer()
? controller->storiesPeer()
: controller->musicPeer()
? controller->musicPeer()
: controller->parentController()->session().user()),
controller->topic(),
controller->sublist(),
controller->migratedPeerId(),
(controller->section().type() == Section::Type::Downloads
? Type::File
: controller->section().type() == Section::Type::Stories
? Type::PhotoVideo
: controller->section().type() == Section::Type::SavedMusic
? Type::MusicFile
: controller->section().mediaType())) {
}
Memento::Memento(not_null<PeerData*> peer, PeerId migratedPeerId, Type type)
: Memento(peer, nullptr, nullptr, migratedPeerId, type) {
}
Memento::Memento(not_null<Data::ForumTopic*> topic, Type type)
: Memento(topic->peer(), topic, nullptr, PeerId(), type) {
}
Memento::Memento(not_null<Data::SavedSublist*> sublist, Type type)
: Memento(sublist->owningHistory()->peer, nullptr, sublist, PeerId(), type) {
}
Memento::Memento(
not_null<PeerData*> peer,
Data::ForumTopic *topic,
Data::SavedSublist *sublist,
PeerId migratedPeerId,
Type type)
: ContentMemento(peer, topic, sublist, migratedPeerId)
, _type(type) {
_searchState.query.type = type;
_searchState.query.peerId = peer->id;
_searchState.query.topicRootId = topic ? topic->rootId() : MsgId();
_searchState.query.monoforumPeerId = sublist
? sublist->sublistPeer()->id
: PeerId();
_searchState.query.migratedPeerId = migratedPeerId;
if (migratedPeerId) {
_searchState.migratedList = Storage::SparseIdsList();
}
}
Section Memento::section() const {
return Section(_type);
}
object_ptr<ContentWidget> Memento::createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) {
auto result = object_ptr<Widget>(
parent,
controller);
result->setInternalState(geometry, this);
return result;
}
Widget::Widget(QWidget *parent, not_null<Controller*> controller)
: ContentWidget(parent, controller) {
_inner = setInnerWidget(object_ptr<InnerWidget>(
this,
controller));
_inner->setScrollHeightValue(scrollHeightValue());
_inner->scrollToRequests(
) | rpl::on_next([this](Ui::ScrollToRequest request) {
scrollTo(request);
}, _inner->lifetime());
}
rpl::producer<SelectedItems> Widget::selectedListValue() const {
return _inner->selectedListValue();
}
void Widget::selectionAction(SelectionAction action) {
_inner->selectionAction(action);
}
rpl::producer<QString> Widget::title() {
if (controller()->key().peer()->sharedMediaInfo() && isStackBottom()) {
return tr::lng_profile_shared_media();
}
return SharedMediaTitle(controller()->section().mediaType())();
}
void Widget::setIsStackBottom(bool isStackBottom) {
ContentWidget::setIsStackBottom(isStackBottom);
_inner->setIsStackBottom(isStackBottom);
}
bool Widget::showInternal(not_null<ContentMemento*> memento) {
if (!controller()->validateMementoPeer(memento)) {
return false;
}
if (const auto mediaMemento = dynamic_cast<Memento*>(memento.get())) {
if (_inner->showInternal(mediaMemento)) {
return true;
}
}
return false;
}
void Widget::setInternalState(
const QRect &geometry,
not_null<Memento*> memento) {
setGeometry(geometry);
Ui::SendPendingMoveResizeEvents(this);
restoreState(memento);
}
std::shared_ptr<ContentMemento> Widget::doCreateMemento() {
auto result = std::make_shared<Memento>(controller());
saveState(result.get());
return result;
}
void Widget::saveState(not_null<Memento*> memento) {
_inner->saveState(memento);
}
void Widget::restoreState(not_null<Memento*> memento) {
_inner->restoreState(memento);
}
} // namespace Info::Media

View File

@@ -0,0 +1,140 @@
/*
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 "info/info_content_widget.h"
#include "storage/storage_shared_media.h"
#include "data/data_search_controller.h"
namespace tr {
template <typename ...Tags>
struct phrase;
} // namespace tr
namespace Data {
class ForumTopic;
} // namespace Data
namespace Info::Media {
using Type = Storage::SharedMediaType;
[[nodiscard]] std::optional<int> TypeToTabIndex(Type type);
[[nodiscard]] Type TabIndexToType(int index);
[[nodiscard]] tr::phrase<> SharedMediaTitle(Type type);
class InnerWidget;
class Memento final : public ContentMemento {
public:
explicit Memento(not_null<Controller*> controller);
Memento(not_null<PeerData*> peer, PeerId migratedPeerId, Type type);
Memento(not_null<Data::ForumTopic*> topic, Type type);
Memento(not_null<Data::SavedSublist*> sublist, Type type);
using SearchState = Api::DelayedSearchController::SavedState;
object_ptr<ContentWidget> createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) override;
[[nodiscard]] Section section() const override;
[[nodiscard]] Type type() const {
return _type;
}
// Only for media, not for downloads.
void setAroundId(FullMsgId aroundId) {
_aroundId = aroundId;
}
[[nodiscard]] FullMsgId aroundId() const {
return _aroundId;
}
void setIdsLimit(int limit) {
_idsLimit = limit;
}
[[nodiscard]] int idsLimit() const {
return _idsLimit;
}
void setScrollTopItem(GlobalMsgId item) {
_scrollTopItem = item;
}
[[nodiscard]] GlobalMsgId scrollTopItem() const {
return _scrollTopItem;
}
void setScrollTopItemPosition(int64 position) {
_scrollTopItemPosition = position;
}
[[nodiscard]] int64 scrollTopItemPosition() const {
return _scrollTopItemPosition;
}
void setScrollTopShift(int shift) {
_scrollTopShift = shift;
}
[[nodiscard]] int scrollTopShift() const {
return _scrollTopShift;
}
void setSearchState(SearchState &&state) {
_searchState = std::move(state);
}
[[nodiscard]] SearchState searchState() {
return std::move(_searchState);
}
private:
Memento(
not_null<PeerData*> peer,
Data::ForumTopic *topic,
Data::SavedSublist *sublist,
PeerId migratedPeerId,
Type type);
Type _type = Type::Photo;
FullMsgId _aroundId;
int _idsLimit = 0;
int64 _scrollTopItemPosition = 0;
GlobalMsgId _scrollTopItem;
int _scrollTopShift = 0;
SearchState _searchState;
};
class Widget final : public ContentWidget {
public:
Widget(
QWidget *parent,
not_null<Controller*> controller);
void setIsStackBottom(bool isStackBottom) override;
bool showInternal(
not_null<ContentMemento*> memento) override;
void setInternalState(
const QRect &geometry,
not_null<Memento*> memento);
rpl::producer<SelectedItems> selectedListValue() const override;
void selectionAction(SelectionAction action) override;
rpl::producer<QString> title() override;
private:
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
std::shared_ptr<ContentMemento> doCreateMemento() override;
InnerWidget *_inner = nullptr;
};
} // namespace Info::Media

View File

@@ -0,0 +1,111 @@
/*
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 "info/members/info_members_widget.h"
#include "info/profile/info_profile_members.h"
#include "info/info_controller.h"
#include "ui/widgets/scroll_area.h"
#include "ui/ui_utility.h"
#include "data/data_peer.h"
#include "data/data_channel.h"
#include "lang/lang_keys.h"
#include "styles/style_info.h"
namespace Info {
namespace Members {
Memento::Memento(not_null<Controller*> controller)
: Memento(
controller->peer(),
controller->migratedPeerId()) {
}
Memento::Memento(not_null<PeerData*> peer, PeerId migratedPeerId)
: ContentMemento(peer, nullptr, nullptr, migratedPeerId) {
}
Section Memento::section() const {
return Section(Section::Type::Members);
}
object_ptr<ContentWidget> Memento::createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) {
auto result = object_ptr<Widget>(
parent,
controller);
result->setInternalState(geometry, this);
return result;
}
void Memento::setState(std::unique_ptr<SavedState> state) {
_state = std::move(state);
}
std::unique_ptr<SavedState> Memento::state() {
return std::move(_state);
}
Memento::~Memento() = default;
Widget::Widget(
QWidget *parent,
not_null<Controller*> controller)
: ContentWidget(parent, controller) {
_inner = setInnerWidget(object_ptr<Profile::Members>(
this,
controller));
}
bool Widget::showInternal(not_null<ContentMemento*> memento) {
if (!controller()->validateMementoPeer(memento)) {
return false;
}
if (auto membersMemento = dynamic_cast<Memento*>(memento.get())) {
restoreState(membersMemento);
return true;
}
return false;
}
void Widget::setInternalState(
const QRect &geometry,
not_null<Memento*> memento) {
setGeometry(geometry);
Ui::SendPendingMoveResizeEvents(this);
restoreState(memento);
}
rpl::producer<QString> Widget::title() {
if (const auto channel = controller()->key().peer()->asChannel()) {
return channel->isMegagroup()
? tr::lng_profile_participants_section()
: tr::lng_profile_subscribers_section();
}
return tr::lng_profile_participants_section();
}
std::shared_ptr<ContentMemento> Widget::doCreateMemento() {
auto result = std::make_shared<Memento>(controller());
saveState(result.get());
return result;
}
void Widget::saveState(not_null<Memento*> memento) {
memento->setScrollTop(scrollTopSave());
memento->setState(_inner->saveState());
}
void Widget::restoreState(not_null<Memento*> memento) {
_inner->restoreState(memento->state());
scrollTopRestore(memento->scrollTop());
}
} // namespace Members
} // namespace Info

View File

@@ -0,0 +1,73 @@
/*
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 "info/info_content_widget.h"
struct PeerListState;
namespace Info {
namespace Profile {
class Members;
struct MembersState;
} // namespace Profile
namespace Members {
using SavedState = Profile::MembersState;
class Memento final : public ContentMemento {
public:
Memento(not_null<Controller*> controller);
Memento(not_null<PeerData*> peer, PeerId migratedPeerId);
object_ptr<ContentWidget> createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) override;
Section section() const override;
void setState(std::unique_ptr<SavedState> state);
std::unique_ptr<SavedState> state();
~Memento();
private:
std::unique_ptr<SavedState> _state;
};
class Widget final : public ContentWidget {
public:
Widget(
QWidget *parent,
not_null<Controller*> controller);
bool showInternal(
not_null<ContentMemento*> memento) override;
void setInternalState(
const QRect &geometry,
not_null<Memento*> memento);
rpl::producer<QString> title() override;
private:
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
std::shared_ptr<ContentMemento> doCreateMemento() override;
Profile::Members *_inner = nullptr;
};
} // namespace Members
} // namespace Info

View File

@@ -0,0 +1,159 @@
/*
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 "info/peer_gifts/info_peer_gifts_collections.h"
#include "api/api_credits.h"
#include "apiwrap.h"
#include "data/data_peer.h"
#include "data/data_star_gift.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/boxes/confirm_box.h"
#include "ui/layers/generic_box.h"
#include "ui/widgets/fields/input_field.h"
#include "window/window_session_controller.h"
#include "styles/style_layers.h"
#include "styles/style_info.h"
namespace Info::PeerGifts {
namespace {
constexpr auto kCollectionNameLimit = 12;
void EditCollectionBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
int id,
Data::SavedStarGiftId addId,
QString currentName,
Fn<void(MTPStarGiftCollection)> finished) {
box->setTitle(id
? tr::lng_gift_collection_edit()
: tr::lng_gift_collection_new_title());
if (!id) {
box->addRow(
object_ptr<Ui::FlatLabel>(
box,
tr::lng_gift_collection_new_text(),
st::collectionAbout));
}
const auto title = box->addRow(
object_ptr<Ui::InputField>(
box,
st::collectionNameField,
tr::lng_gift_collection_new_ph(),
currentName));
title->setMaxLength(kCollectionNameLimit * 2);
box->setFocusCallback([=] {
title->setFocusFast();
});
Ui::AddLengthLimitLabel(title, kCollectionNameLimit);
const auto show = navigation->uiShow();
const auto session = &peer->session();
const auto creating = std::make_shared<bool>(false);
const auto submit = [=] {
if (*creating) {
return;
}
const auto text = title->getLastText().trimmed();
if (text.isEmpty() || text.size() > kCollectionNameLimit) {
title->showError();
return;
}
*creating = true;
auto ids = QVector<MTPInputSavedStarGift>();
if (addId) {
ids.push_back(Api::InputSavedStarGiftId(addId));
}
const auto weak = base::make_weak(box);
const auto done = [=](const MTPStarGiftCollection &result) {
*creating = false;
if (const auto onstack = finished) {
onstack(result);
}
if (const auto strong = weak.get()) {
strong->closeBox();
}
};
const auto fail = [=](const MTP::Error &error) {
*creating = false;
const auto &type = error.type();
if (type == u"COLLECTIONS_TOO_MANY"_q) {
show->show(Ui::MakeInformBox({
.text = tr::lng_gift_collection_limit_text(),
.confirmText = tr::lng_box_ok(),
.title = tr::lng_gift_collection_limit_title(),
}));
if (const auto strong = weak.get()) {
strong->closeBox();
}
} else {
show->showToast(error.type());
}
};
if (id) {
using Flag = MTPpayments_UpdateStarGiftCollection::Flag;
session->api().request(MTPpayments_UpdateStarGiftCollection(
MTP_flags(Flag::f_title),
peer->input(),
MTP_int(id),
MTP_string(text),
MTPVector<MTPInputSavedStarGift>(),
MTPVector<MTPInputSavedStarGift>(),
MTPVector<MTPInputSavedStarGift>()
)).done(done).fail(fail).send();
} else {
session->api().request(MTPpayments_CreateStarGiftCollection(
peer->input(),
MTP_string(text),
MTP_vector<MTPInputSavedStarGift>(ids)
)).done(done).fail(fail).send();
}
};
title->submits() | rpl::on_next(submit, title->lifetime());
auto text = id
? tr::lng_settings_save()
: tr::lng_gift_collection_new_create();
box->addButton(std::move(text), submit);
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
}
} // namespace
void NewCollectionBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
Data::SavedStarGiftId addId,
Fn<void(MTPStarGiftCollection)> added) {
EditCollectionBox(box, navigation, peer, 0, addId, QString(), added);
}
void EditCollectionNameBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
int id,
QString current,
Fn<void(QString)> done) {
EditCollectionBox(box, navigation, peer, id, {}, current, [=](
const MTPStarGiftCollection &result) {
done(qs(result.data().vtitle()));
});
}
} // namespace Info::PeerGifts

View File

@@ -0,0 +1,39 @@
/*
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
namespace Data {
class SavedStarGiftId;
} // namespace Data
namespace Ui {
class GenericBox;
} // namespace Ui
namespace Window {
class SessionNavigation;
} // namespace Window
namespace Info::PeerGifts {
void NewCollectionBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
Data::SavedStarGiftId addId,
Fn<void(MTPStarGiftCollection)> added);
void EditCollectionNameBox(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer,
int id,
QString current,
Fn<void(QString)> done);
} // namespace Info::PeerGifts

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,308 @@
/*
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 "base/qt/qt_compare.h"
#include "base/timer.h"
#include "data/data_star_gift.h"
#include "ui/abstract_button.h"
#include "ui/effects/premium_stars_colored.h"
#include "ui/text/custom_emoji_helper.h"
#include "ui/text/text.h"
class StickerPremiumMark;
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace Data {
struct UniqueGift;
struct CreditsHistoryEntry;
class SavedStarGiftId;
} // namespace Data
namespace HistoryView {
class StickerPlayer;
} // namespace HistoryView
namespace Main {
class Session;
} // namespace Main
namespace Overview::Layout {
class Checkbox;
} // namespace Overview::Layout
namespace Ui {
class DynamicImage;
} // namespace Ui
namespace Ui::Text {
class CustomEmoji;
} // namespace Ui::Text
namespace Window {
class SessionController;
} // namespace Window
namespace Info::PeerGifts {
struct Tag {
explicit Tag(not_null<PeerData*> peer, int collectionId = 0)
: peer(peer)
, collectionId(collectionId) {
}
not_null<PeerData*> peer;
int collectionId = 0;
};
struct GiftTypePremium {
int64 cost = 0;
QString currency;
int stars = 0;
int months = 0;
int discountPercent = 0;
[[nodiscard]] friend inline bool operator==(
const GiftTypePremium &,
const GiftTypePremium &) = default;
};
struct GiftTypeStars {
Data::SavedStarGiftId transferId;
Data::StarGift info;
PeerData *from = nullptr;
TimeId date = 0;
bool pinnedSelection : 1 = false;
bool forceTon : 1 = false;
bool userpic : 1 = false;
bool pinned : 1 = false;
bool hidden : 1 = false;
bool resale : 1 = false;
bool mine : 1 = false;
[[nodiscard]] friend inline bool operator==(
const GiftTypeStars&,
const GiftTypeStars&) = default;
};
[[nodiscard]] rpl::producer<std::vector<GiftTypeStars>> GiftsStars(
not_null<Main::Session*> session,
not_null<PeerData*> peer);
struct GiftDescriptor : std::variant<GiftTypePremium, GiftTypeStars> {
using variant::variant;
[[nodiscard]] friend inline bool operator==(
const GiftDescriptor&,
const GiftDescriptor&) = default;
};
struct GiftSendDetails {
GiftDescriptor descriptor;
TextWithEntities text;
uint64 randomId = 0;
bool anonymous = false;
bool upgraded = false;
bool byStars = false;
};
struct GiftBadge {
QString text;
QColor bg1;
QColor bg2 = QColor(0, 0, 0, 0);
QColor border = QColor(0, 0, 0, 0);
QColor fg;
bool gradient = false;
bool small = false;
explicit operator bool() const {
return !text.isEmpty();
}
friend std::strong_ordering operator<=>(
const GiftBadge &a,
const GiftBadge &b);
friend inline bool operator==(
const GiftBadge &,
const GiftBadge &) = default;
};
enum class GiftButtonMode : uint8 {
Full,
Minimal,
Selection,
};
enum class GiftSelectionMode : uint8 {
Border,
Inset,
Check,
};
class GiftButtonDelegate {
public:
[[nodiscard]] virtual TextWithEntities star() = 0;
[[nodiscard]] virtual TextWithEntities monostar() = 0;
[[nodiscard]] virtual TextWithEntities monoton() = 0;
[[nodiscard]] virtual TextWithEntities ministar() = 0;
[[nodiscard]] virtual Ui::Text::MarkedContext textContext() = 0;
[[nodiscard]] virtual QSize buttonSize() = 0;
[[nodiscard]] virtual QMargins buttonExtend() const = 0;
[[nodiscard]] virtual auto buttonPatternEmoji(
not_null<Data::UniqueGift*> unique,
Fn<void()> repaint)
-> std::unique_ptr<Ui::Text::CustomEmoji> = 0;
[[nodiscard]] virtual QImage background() = 0;
[[nodiscard]] virtual rpl::producer<not_null<DocumentData*>> sticker(
const GiftDescriptor &descriptor) = 0;
[[nodiscard]] virtual not_null<StickerPremiumMark*> hiddenMark() = 0;
[[nodiscard]] virtual QImage cachedBadge(const GiftBadge &badge) = 0;
[[nodiscard]] virtual bool amPremium() = 0;
virtual void invalidateCache() = 0;
};
class GiftButton final : public Ui::AbstractButton {
public:
GiftButton(QWidget *parent, not_null<GiftButtonDelegate*> delegate);
~GiftButton();
using Mode = GiftButtonMode;
void setDescriptor(const GiftDescriptor &descriptor, Mode mode);
void setGeometry(QRect inner, QMargins extend);
void toggleSelected(
bool selected,
GiftSelectionMode selectionMode = GiftSelectionMode::Border,
anim::type animated = anim::type::normal);
[[nodiscard]] rpl::producer<QPoint> contextMenuRequests() const;
[[nodiscard]] rpl::producer<QMouseEvent*> mouseEvents();
private:
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void contextMenuEvent(QContextMenuEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void paintBackground(QPainter &p, const QImage &background);
void cacheUniqueBackground(
not_null<Data::UniqueGift*> unique,
int width,
int height);
void refreshLocked();
void setDocument(not_null<DocumentData*> document);
[[nodiscard]] QMargins currentExtend() const;
[[nodiscard]] bool small() const;
void onStateChanged(State was, StateChangeSource source) override;
void unsubscribe();
const not_null<GiftButtonDelegate*> _delegate;
rpl::event_stream<QPoint> _contextMenuRequests;
rpl::event_stream<QMouseEvent*> _mouseEvents;
QImage _hiddenBgCache;
GiftDescriptor _descriptor;
Ui::Text::String _text;
Ui::Text::String _price;
Ui::Text::String _byStars;
std::shared_ptr<Ui::DynamicImage> _userpic;
QImage _uniqueBackgroundCache;
QImage _tonIcon;
std::unique_ptr<Ui::Text::CustomEmoji> _uniquePatternEmoji;
base::flat_map<float64, QImage> _uniquePatternCache;
std::optional<Ui::Premium::ColoredMiniStars> _stars;
Ui::Animations::Simple _selectedAnimation;
std::unique_ptr<Overview::Layout::Checkbox> _check;
int _resalePrice = 0;
GiftButtonMode _mode = GiftButtonMode::Full;
GiftSelectionMode _selectionMode = GiftSelectionMode::Border;
bool _subscribed : 1 = false;
bool _patterned : 1 = false;
bool _selected : 1 = false;
bool _locked : 1 = false;
bool _mouseEventsAreListening = false;
base::Timer _lockedTimer;
TimeId _lockedUntilDate = 0;
QRect _button;
QMargins _extend;
DocumentData *_resolvedDocument = nullptr;
std::unique_ptr<HistoryView::StickerPlayer> _player;
DocumentData *_playerDocument = nullptr;
rpl::lifetime _mediaLifetime;
rpl::lifetime _documentLifetime;
};
class Delegate final : public GiftButtonDelegate {
public:
Delegate(not_null<Main::Session*> session, GiftButtonMode mode);
Delegate(Delegate &&other);
~Delegate();
TextWithEntities star() override;
TextWithEntities monostar() override;
TextWithEntities monoton() override;
TextWithEntities ministar() override;
Ui::Text::MarkedContext textContext() override;
QSize buttonSize() override;
QMargins buttonExtend() const override;
auto buttonPatternEmoji(
not_null<Data::UniqueGift*> unique,
Fn<void()> repaint)
-> std::unique_ptr<Ui::Text::CustomEmoji> override;
QImage background() override;
rpl::producer<not_null<DocumentData*>> sticker(
const GiftDescriptor &descriptor) override;
not_null<StickerPremiumMark*> hiddenMark() override;
QImage cachedBadge(const GiftBadge &badge) override;
bool amPremium() override;
void invalidateCache() override;
private:
const not_null<Main::Session*> _session;
std::unique_ptr<StickerPremiumMark> _hiddenMark;
base::flat_map<GiftBadge, QImage> _badges;
QSize _single;
QImage _bg;
GiftButtonMode _mode = GiftButtonMode::Full;
Ui::Text::CustomEmojiHelper _emojiHelper;
TextWithEntities _ministarEmoji;
TextWithEntities _starEmoji;
};
[[nodiscard]] DocumentData *LookupGiftSticker(
not_null<Main::Session*> session,
const GiftDescriptor &descriptor);
[[nodiscard]] rpl::producer<not_null<DocumentData*>> GiftStickerValue(
not_null<Main::Session*> session,
const GiftDescriptor &descriptor);
[[nodiscard]] QImage ValidateRotatedBadge(
const GiftBadge &badge,
QMargins padding);
void SelectGiftToUnpin(
std::shared_ptr<ChatHelpers::Show> show,
const std::vector<Data::CreditsHistoryEntry> &pinned,
Fn<void(Data::SavedStarGiftId)> chosen);
} // namespace Info::PeerGifts

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,127 @@
/*
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 "data/data_star_gift.h"
#include "info/info_content_widget.h"
class UserData;
struct PeerListState;
namespace Ui {
class RpWidget;
template <typename Widget>
class SlideWrap;
} // namespace Ui
namespace Info::PeerGifts {
struct ListState {
std::vector<Data::SavedStarGift> list;
QString offset;
};
struct Filter {
bool sortByValue : 1 = false;
bool skipUnlimited : 1 = false;
bool skipLimited : 1 = false;
bool skipUpgradable : 1 = false;
bool skipUnique : 1 = false;
bool skipSaved : 1 = false;
bool skipUnsaved : 1 = false;
[[nodiscard]] bool skipsSomething() const {
return skipLimited
|| skipUnlimited
|| skipSaved
|| skipUnsaved
|| skipUpgradable
|| skipUnique;
}
friend inline bool operator==(Filter, Filter) = default;
};
struct Descriptor {
Filter filter;
int collectionId = 0;
friend inline bool operator==(
const Descriptor &,
const Descriptor &) = default;
};
class InnerWidget;
class Memento final : public ContentMemento {
public:
Memento(not_null<Controller*> controller);
Memento(not_null<PeerData*> peer, int collectionId);
~Memento();
object_ptr<ContentWidget> createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) override;
Section section() const override;
void setListState(std::unique_ptr<ListState> state);
std::unique_ptr<ListState> listState();
private:
std::unique_ptr<ListState> _listState;
};
class Widget final : public ContentWidget {
public:
Widget(QWidget *parent, not_null<Controller*> controller);
[[nodiscard]] not_null<PeerData*> peer() const;
bool showInternal(
not_null<ContentMemento*> memento) override;
void setInternalState(
const QRect &geometry,
not_null<Memento*> memento);
void fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) override;
rpl::producer<QString> title() override;
rpl::producer<bool> desiredBottomShadowVisibility() override;
void showFinished() override;
private:
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
std::shared_ptr<ContentMemento> doCreateMemento() override;
void setupNotifyCheckbox(int wasBottomHeight, bool enabled);
void setupBottomButton(int wasBottomHeight);
void refreshBottom();
InnerWidget *_inner = nullptr;
QPointer<Ui::SlideWrap<Ui::RpWidget>> _pinnedToBottom;
rpl::variable<bool> _hasPinnedToBottom;
rpl::variable<bool> _emptyCollectionShown;
rpl::variable<Descriptor> _descriptor;
std::optional<bool> _notifyEnabled;
bool _shown = false;
};
[[nodiscard]] std::shared_ptr<Info::Memento> Make(
not_null<PeerData*> peer,
int collectionId = 0);
} // namespace Info::PeerGifts

View File

@@ -0,0 +1,673 @@
/*
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 "info/polls/info_polls_results_inner_widget.h"
#include "info/polls/info_polls_results_widget.h"
#include "lang/lang_keys.h"
#include "core/ui_integration.h"
#include "data/data_peer.h"
#include "data/data_poll.h"
#include "data/data_session.h"
#include "ui/controls/peer_list_dummy.h"
#include "ui/widgets/buttons.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/text/text_utilities.h"
#include "ui/vertical_list.h"
#include "boxes/peer_list_box.h"
#include "main/main_session.h"
#include "history/history.h"
#include "history/history_item.h"
#include "styles/style_layers.h"
#include "styles/style_info.h"
namespace Info::Polls {
namespace {
constexpr auto kFirstPage = 15;
constexpr auto kPerPage = 50;
constexpr auto kLeavePreloaded = 5;
class ListDelegate final : public PeerListContentDelegate {
public:
void peerListSetTitle(rpl::producer<QString> title) override;
void peerListSetAdditionalTitle(rpl::producer<QString> title) override;
bool peerListIsRowChecked(not_null<PeerListRow*> row) override;
int peerListSelectedRowsCount() override;
void peerListScrollToTop() override;
void peerListAddSelectedPeerInBunch(
not_null<PeerData*> peer) override;
void peerListAddSelectedRowInBunch(
not_null<PeerListRow*> row) override;
void peerListFinishSelectedRowsBunch() override;
void peerListSetDescription(
object_ptr<Ui::FlatLabel> description) override;
std::shared_ptr<Main::SessionShow> peerListUiShow() override;
};
void ListDelegate::peerListSetTitle(rpl::producer<QString> title) {
}
void ListDelegate::peerListSetAdditionalTitle(rpl::producer<QString> title) {
}
bool ListDelegate::peerListIsRowChecked(not_null<PeerListRow*> row) {
return false;
}
int ListDelegate::peerListSelectedRowsCount() {
return 0;
}
void ListDelegate::peerListScrollToTop() {
}
void ListDelegate::peerListAddSelectedPeerInBunch(not_null<PeerData*> peer) {
Unexpected("Item selection in Info::Profile::Members.");
}
void ListDelegate::peerListAddSelectedRowInBunch(not_null<PeerListRow*> row) {
Unexpected("Item selection in Info::Profile::Members.");
}
void ListDelegate::peerListFinishSelectedRowsBunch() {
}
void ListDelegate::peerListSetDescription(
object_ptr<Ui::FlatLabel> description) {
description.destroy();
}
std::shared_ptr<Main::SessionShow> ListDelegate::peerListUiShow() {
Unexpected("...ListDelegate::peerListUiShow");
}
} // namespace
class ListController final : public PeerListController {
public:
ListController(
not_null<Main::Session*> session,
not_null<PollData*> poll,
FullMsgId context,
QByteArray option);
Main::Session &session() const override;
void prepare() override;
void rowClicked(not_null<PeerListRow*> row) override;
void loadMoreRows() override;
void allowLoadMore();
void collapse();
[[nodiscard]] auto showPeerInfoRequests() const
-> rpl::producer<not_null<PeerData*>>;
[[nodiscard]] rpl::producer<int> scrollToRequests() const;
[[nodiscard]] rpl::producer<int> count() const;
[[nodiscard]] rpl::producer<int> fullCount() const;
[[nodiscard]] rpl::producer<int> loadMoreCount() const;
std::unique_ptr<PeerListState> saveState() const override;
void restoreState(std::unique_ptr<PeerListState> state) override;
std::unique_ptr<PeerListRow> createRestoredRow(
not_null<PeerData*> peer) override;
void scrollTo(int y);
private:
struct SavedState : SavedStateBase {
QString offset;
QString loadForOffset;
int leftToLoad = 0;
int fullCount = 0;
std::vector<not_null<PeerData*>> preloaded;
bool wasLoading = false;
};
bool appendRow(not_null<PeerData*> peer);
std::unique_ptr<PeerListRow> createRow(not_null<PeerData*> peer) const;
void addPreloaded();
bool addPreloadedPage();
void preloadedAdded();
const not_null<Main::Session*> _session;
const not_null<PollData*> _poll;
const FullMsgId _context;
const QByteArray _option;
MTP::Sender _api;
QString _offset;
mtpRequestId _loadRequestId = 0;
QString _loadForOffset;
std::vector<not_null<PeerData*>> _preloaded;
rpl::variable<int> _count = 0;
rpl::variable<int> _fullCount;
rpl::variable<int> _leftToLoad;
rpl::event_stream<not_null<PeerData*>> _showPeerInfoRequests;
rpl::event_stream<int> _scrollToRequests;
};
ListController::ListController(
not_null<Main::Session*> session,
not_null<PollData*> poll,
FullMsgId context,
QByteArray option)
: _session(session)
, _poll(poll)
, _context(context)
, _option(option)
, _api(&_session->mtp()) {
const auto i = ranges::find(poll->answers, option, &PollAnswer::option);
Assert(i != poll->answers.end());
_fullCount = i->votes;
_leftToLoad = i->votes;
}
Main::Session &ListController::session() const {
return *_session;
}
void ListController::prepare() {
delegate()->peerListRefreshRows();
}
void ListController::loadMoreRows() {
if (_loadRequestId
|| !_leftToLoad.current()
|| (!_offset.isEmpty() && _loadForOffset != _offset)
|| !_preloaded.empty()) {
return;
}
const auto item = session().data().message(_context);
if (!item || !item->isRegular()) {
_leftToLoad = 0;
return;
}
using Flag = MTPmessages_GetPollVotes::Flag;
const auto flags = Flag::f_option
| (_offset.isEmpty() ? Flag(0) : Flag::f_offset);
const auto limit = _offset.isEmpty() ? kFirstPage : kPerPage;
_loadRequestId = _api.request(MTPmessages_GetPollVotes(
MTP_flags(flags),
item->history()->peer->input(),
MTP_int(item->id),
MTP_bytes(_option),
MTP_string(_offset),
MTP_int(limit)
)).done([=](const MTPmessages_VotesList &result) {
const auto count = result.match([&](
const MTPDmessages_votesList &data) {
_offset = data.vnext_offset().value_or_empty();
auto &owner = session().data();
owner.processUsers(data.vusers());
owner.processChats(data.vchats());
auto add = limit - kLeavePreloaded;
for (const auto &vote : data.vvotes().v) {
vote.match([&](const auto &data) {
const auto peer = owner.peer(peerFromMTP(data.vpeer()));
if (peer->isMinimalLoaded()) {
if (add) {
appendRow(peer);
--add;
} else {
_preloaded.push_back(peer);
}
}
});
}
return data.vcount().v;
});
if (_offset.isEmpty()) {
addPreloaded();
_fullCount = delegate()->peerListFullRowsCount();
_leftToLoad = 0;
} else {
_count = delegate()->peerListFullRowsCount();
_fullCount = count;
_leftToLoad = count - delegate()->peerListFullRowsCount();
delegate()->peerListRefreshRows();
}
_loadRequestId = 0;
}).fail([=] {
_loadRequestId = 0;
}).send();
}
void ListController::allowLoadMore() {
if (!addPreloadedPage()) {
_loadForOffset = _offset;
addPreloaded();
loadMoreRows();
}
}
void ListController::collapse() {
const auto count = delegate()->peerListFullRowsCount();
if (count <= kFirstPage) {
return;
}
const auto remove = count - (kFirstPage - kLeavePreloaded);
ranges::actions::reverse(_preloaded);
_preloaded.reserve(_preloaded.size() + remove);
for (auto i = 0; i != remove; ++i) {
const auto row = delegate()->peerListRowAt(count - i - 1);
_preloaded.push_back(row->peer());
delegate()->peerListRemoveRow(row);
}
ranges::actions::reverse(_preloaded);
delegate()->peerListRefreshRows();
const auto now = count - remove;
_count = now;
_leftToLoad = _fullCount.current() - now;
}
void ListController::addPreloaded() {
for (const auto peer : base::take(_preloaded)) {
appendRow(peer);
}
preloadedAdded();
}
bool ListController::addPreloadedPage() {
if (_preloaded.size() < kPerPage + kLeavePreloaded) {
return false;
}
const auto from = begin(_preloaded);
const auto till = from + kPerPage;
for (auto i = from; i != till; ++i) {
appendRow(*i);
}
_preloaded.erase(from, till);
preloadedAdded();
return true;
}
void ListController::preloadedAdded() {
_count = delegate()->peerListFullRowsCount();
_leftToLoad = _fullCount.current() - _count.current();
delegate()->peerListRefreshRows();
}
auto ListController::showPeerInfoRequests() const
-> rpl::producer<not_null<PeerData*>> {
return _showPeerInfoRequests.events();
}
rpl::producer<int> ListController::scrollToRequests() const {
return _scrollToRequests.events();
}
rpl::producer<int> ListController::count() const {
return _count.value();
}
rpl::producer<int> ListController::fullCount() const {
return _fullCount.value();
}
rpl::producer<int> ListController::loadMoreCount() const {
const auto initial = (_fullCount.current() <= kFirstPage)
? _fullCount.current()
: (kFirstPage - kLeavePreloaded);
return rpl::combine(
_count.value(),
_leftToLoad.value()
) | rpl::map([=](int count, int leftToLoad) {
return (count > 0) ? leftToLoad : (leftToLoad - initial);
});
}
auto ListController::saveState() const -> std::unique_ptr<PeerListState> {
auto result = PeerListController::saveState();
auto my = std::make_unique<SavedState>();
my->offset = _offset;
my->fullCount = _fullCount.current();
my->leftToLoad = _leftToLoad.current();
my->preloaded = _preloaded;
my->wasLoading = (_loadRequestId != 0);
my->loadForOffset = _loadForOffset;
result->controllerState = std::move(my);
return result;
}
void ListController::restoreState(std::unique_ptr<PeerListState> state) {
auto typeErasedState = state
? state->controllerState.get()
: nullptr;
if (const auto my = dynamic_cast<SavedState*>(typeErasedState)) {
if (const auto requestId = base::take(_loadRequestId)) {
_api.request(requestId).cancel();
}
_offset = my->offset;
_loadForOffset = my->loadForOffset;
_preloaded = std::move(my->preloaded);
_count = int(state->list.size());
_fullCount = my->fullCount;
_leftToLoad = my->leftToLoad;
if (my->wasLoading) {
loadMoreRows();
}
PeerListController::restoreState(std::move(state));
}
}
std::unique_ptr<PeerListRow> ListController::createRestoredRow(
not_null<PeerData*> peer) {
return createRow(peer);
}
void ListController::rowClicked(not_null<PeerListRow*> row) {
_showPeerInfoRequests.fire(row->peer());
}
bool ListController::appendRow(not_null<PeerData*> peer) {
if (delegate()->peerListFindRow(peer->id.value)) {
return false;
}
delegate()->peerListAppendRow(createRow(peer));
return true;
}
std::unique_ptr<PeerListRow> ListController::createRow(
not_null<PeerData*> peer) const {
auto row = std::make_unique<PeerListRow>(peer);
row->setCustomStatus(QString());
return row;
}
void ListController::scrollTo(int y) {
_scrollToRequests.fire_copy(y);
}
ListController *CreateAnswerRows(
not_null<Ui::VerticalLayout*> container,
rpl::producer<int> visibleTop,
not_null<Main::Session*> session,
not_null<PollData*> poll,
FullMsgId context,
const PollAnswer &answer) {
using namespace rpl::mappers;
if (!answer.votes) {
return nullptr;
}
const auto delegate = container->lifetime().make_state<ListDelegate>();
const auto controller = container->lifetime().make_state<ListController>(
session,
poll,
context,
answer.option);
const auto percent = answer.votes * 100 / poll->totalVoters;
const auto phrase = poll->quiz()
? tr::lng_polls_answers_count
: tr::lng_polls_votes_count;
const auto sampleText = phrase(
tr::now,
lt_count_decimal,
answer.votes);
const auto &font = st::boxDividerLabel.style.font;
const auto sampleWidth = font->width(sampleText);
const auto rightSkip = sampleWidth + font->spacew * 4;
const auto headerWrap = container->add(
object_ptr<Ui::RpWidget>(
container));
container->add(object_ptr<Ui::FixedHeightWidget>(
container,
st::boxLittleSkip));
controller->setStyleOverrides(&st::infoCommonGroupsList);
const auto content = container->add(object_ptr<PeerListContent>(
container,
controller));
delegate->setContent(content);
controller->setDelegate(delegate);
const auto count = (answer.votes <= kFirstPage)
? answer.votes
: (kFirstPage - kLeavePreloaded);
const auto placeholder = container->add(object_ptr<PeerListDummy>(
container,
count,
st::infoCommonGroupsList));
controller->count(
) | rpl::filter(_1 > 0) | rpl::on_next([=] {
delete placeholder;
}, placeholder->lifetime());
const auto header = Ui::CreateChild<Ui::DividerLabel>(
container.get(),
object_ptr<Ui::FlatLabel>(
container,
rpl::single(
TextWithEntities(answer.text)
.append(QString::fromUtf8(" \xe2\x80\x94 "))
.append(QString::number(percent))
.append('%')),
st::boxDividerLabel,
st::defaultPopupMenu,
Core::TextContext({ .session = session })),
style::margins(
st::pollResultsHeaderPadding.left(),
st::pollResultsHeaderPadding.top(),
st::pollResultsHeaderPadding.right() + rightSkip,
st::pollResultsHeaderPadding.bottom()));
const auto votes = Ui::CreateChild<Ui::FlatLabel>(
header,
phrase(
lt_count_decimal,
controller->fullCount() | rpl::map(_1 + 0.)),
st::pollResultsVotesCount);
const auto collapse = Ui::CreateChild<Ui::LinkButton>(
header,
tr::lng_polls_votes_collapse(tr::now),
st::defaultLinkButton);
collapse->setClickedCallback([=] {
controller->scrollTo(headerWrap->y());
controller->collapse();
});
rpl::combine(
controller->fullCount(),
controller->count()
) | rpl::on_next([=](int fullCount, int count) {
const auto many = (fullCount > kFirstPage)
&& (count > kFirstPage - kLeavePreloaded);
collapse->setVisible(many);
votes->setVisible(!many);
}, collapse->lifetime());
headerWrap->widthValue(
) | rpl::on_next([=](int width) {
header->resizeToWidth(width);
votes->moveToRight(
st::pollResultsHeaderPadding.right(),
st::pollResultsHeaderPadding.top(),
width);
collapse->moveToRight(
st::pollResultsHeaderPadding.right(),
st::pollResultsHeaderPadding.top(),
width);
}, header->lifetime());
header->heightValue(
) | rpl::on_next([=](int height) {
headerWrap->resize(headerWrap->width(), height);
}, header->lifetime());
auto moreTopWidget = object_ptr<Ui::RpWidget>(container);
moreTopWidget->resize(0, 0);
const auto moreTop = container->add(std::move(moreTopWidget));
const auto more = container->add(
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
container,
object_ptr<Ui::SettingsButton>(
container,
tr::lng_polls_show_more(
lt_count_decimal,
controller->loadMoreCount() | rpl::map(_1 + 0.),
tr::upper),
st::pollResultsShowMore)));
more->entity()->setClickedCallback([=] {
controller->allowLoadMore();
});
controller->loadMoreCount(
) | rpl::map(_1 > 0) | rpl::on_next([=](bool visible) {
more->toggle(visible, anim::type::instant);
}, more->lifetime());
container->add(object_ptr<Ui::FixedHeightWidget>(
container,
st::boxLittleSkip));
rpl::combine(
std::move(visibleTop),
headerWrap->geometryValue(),
moreTop->topValue()
) | rpl::filter([=](int, QRect headerRect, int moreTop) {
return moreTop >= headerRect.y() + headerRect.height();
}) | rpl::on_next([=](
int visibleTop,
QRect headerRect,
int moreTop) {
const auto skip = st::pollResultsHeaderPadding.top()
- st::pollResultsHeaderPadding.bottom();
const auto top = std::clamp(
visibleTop - skip,
headerRect.y(),
moreTop - headerRect.height());
header->move(0, top);
}, header->lifetime());
return controller;
}
InnerWidget::InnerWidget(
QWidget *parent,
not_null<Controller*> controller,
not_null<PollData*> poll,
FullMsgId contextId)
: RpWidget(parent)
, _controller(controller)
, _poll(poll)
, _contextId(contextId)
, _content(this) {
setupContent();
}
void InnerWidget::visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) {
setChildVisibleTopBottom(_content, visibleTop, visibleBottom);
_visibleTop = visibleTop;
}
void InnerWidget::saveState(not_null<Memento*> memento) {
auto states = base::flat_map<
QByteArray,
std::unique_ptr<PeerListState>>();
for (const auto &[option, controller] : _sections) {
states[option] = controller->saveState();
}
memento->setListStates(std::move(states));
}
void InnerWidget::restoreState(not_null<Memento*> memento) {
auto states = memento->listStates();
for (const auto &[option, controller] : _sections) {
const auto i = states.find(option);
if (i != end(states)) {
controller->restoreState(std::move(i->second));
}
}
}
int InnerWidget::desiredHeight() const {
auto desired = 0;
//auto count = qMax(_user->commonChatsCount(), 1);
//desired += qMax(count, _list->fullRowsCount())
// * st::infoCommonGroupsList.item.height;
return qMax(height(), desired);
}
void InnerWidget::setupContent() {
_content->add(
object_ptr<Ui::FlatLabel>(
_content,
rpl::single(_poll->question),
st::pollResultsQuestion,
st::defaultPopupMenu,
Core::TextContext({ .session = &_controller->session() })),
st::boxRowPadding);
Ui::AddSkip(_content, st::boxLittleSkip / 2);
_content->add(
object_ptr<Ui::FlatLabel>(
_content,
tr::lng_polls_votes_count(
lt_count_decimal,
rpl::single(float64(_poll->totalVoters))),
st::boxDividerLabel),
st::boxRowPadding);
Ui::AddSkip(_content, st::boxLittleSkip);
for (const auto &answer : _poll->answers) {
const auto session = &_controller->session();
const auto controller = CreateAnswerRows(
_content,
_visibleTop.value(),
session,
_poll,
_contextId,
answer);
if (!controller) {
continue;
}
controller->showPeerInfoRequests(
) | rpl::start_to_stream(
_showPeerInfoRequests,
lifetime());
controller->scrollToRequests(
) | rpl::on_next([=](int y) {
_scrollToRequests.fire({ y, -1 });
}, lifetime());
_sections.emplace(answer.option, controller);
}
widthValue(
) | rpl::on_next([=](int newWidth) {
_content->resizeToWidth(newWidth);
}, _content->lifetime());
_content->heightValue(
) | rpl::on_next([=](int height) {
resize(width(), height);
}, _content->lifetime());
}
rpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {
return _scrollToRequests.events();
}
auto InnerWidget::showPeerInfoRequests() const
-> rpl::producer<not_null<PeerData*>> {
return _showPeerInfoRequests.events();
}
} // namespace Info::Polls

View File

@@ -0,0 +1,73 @@
/*
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/rp_widget.h"
#include "ui/widgets/scroll_area.h"
#include "base/object_ptr.h"
namespace Ui {
class VerticalLayout;
} // namespace Ui
namespace Info {
class Controller;
} // namespace Info
namespace Info::Polls {
class Memento;
class ListController;
class InnerWidget final : public Ui::RpWidget {
public:
InnerWidget(
QWidget *parent,
not_null<Controller*> controller,
not_null<PollData*> poll,
FullMsgId contextId);
[[nodiscard]] not_null<PollData*> poll() const {
return _poll;
}
[[nodiscard]] FullMsgId contextId() const {
return _contextId;
}
[[nodiscard]] auto scrollToRequests() const
-> rpl::producer<Ui::ScrollToRequest>;
[[nodiscard]] auto showPeerInfoRequests() const
-> rpl::producer<not_null<PeerData*>>;
[[nodiscard]] int desiredHeight() const;
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
protected:
void visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) override;
private:
void setupContent();
not_null<Controller*> _controller;
not_null<PollData*> _poll;
FullMsgId _contextId;
object_ptr<Ui::VerticalLayout> _content;
base::flat_map<QByteArray, not_null<ListController*>> _sections;
rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
rpl::event_stream<not_null<PeerData*>> _showPeerInfoRequests;
rpl::variable<int> _visibleTop = 0;
};
} // namespace Info::Polls

View File

@@ -0,0 +1,115 @@
/*
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 "info/polls/info_polls_results_widget.h"
#include "info/polls/info_polls_results_inner_widget.h"
#include "boxes/peer_list_box.h"
#include "lang/lang_keys.h"
#include "data/data_poll.h"
#include "ui/ui_utility.h"
namespace Info::Polls {
Memento::Memento(not_null<PollData*> poll, FullMsgId contextId)
: ContentMemento(poll, contextId) {
}
Section Memento::section() const {
return Section(Section::Type::PollResults);
}
void Memento::setListStates(base::flat_map<
QByteArray,
std::unique_ptr<PeerListState>> states) {
_listStates = std::move(states);
}
auto Memento::listStates()
-> base::flat_map<QByteArray, std::unique_ptr<PeerListState>> {
return std::move(_listStates);
}
object_ptr<ContentWidget> Memento::createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) {
auto result = object_ptr<Widget>(parent, controller);
result->setInternalState(geometry, this);
return result;
}
Memento::~Memento() = default;
Widget::Widget(QWidget *parent, not_null<Controller*> controller)
: ContentWidget(parent, controller)
, _inner(setInnerWidget(
object_ptr<InnerWidget>(
this,
controller,
controller->poll(),
controller->pollContextId()))) {
_inner->showPeerInfoRequests(
) | rpl::on_next([=](not_null<PeerData*> peer) {
controller->showPeerInfo(peer);
}, _inner->lifetime());
_inner->scrollToRequests(
) | rpl::on_next([=](const Ui::ScrollToRequest &request) {
scrollTo(request);
}, _inner->lifetime());
}
not_null<PollData*> Widget::poll() const {
return _inner->poll();
}
FullMsgId Widget::contextId() const {
return _inner->contextId();
}
bool Widget::showInternal(not_null<ContentMemento*> memento) {
//if (const auto myMemento = dynamic_cast<Memento*>(memento.get())) {
// Assert(myMemento->self() == self());
// if (_inner->showInternal(myMemento)) {
// return true;
// }
//}
return false;
}
void Widget::setInternalState(
const QRect &geometry,
not_null<Memento*> memento) {
setGeometry(geometry);
Ui::SendPendingMoveResizeEvents(this);
restoreState(memento);
}
rpl::producer<QString> Widget::title() {
return poll()->quiz()
? tr::lng_polls_quiz_results_title()
: tr::lng_polls_poll_results_title();
}
std::shared_ptr<ContentMemento> Widget::doCreateMemento() {
auto result = std::make_shared<Memento>(poll(), contextId());
saveState(result.get());
return result;
}
void Widget::saveState(not_null<Memento*> memento) {
memento->setScrollTop(scrollTopSave());
_inner->saveState(memento);
}
void Widget::restoreState(not_null<Memento*> memento) {
_inner->restoreState(memento);
scrollTopRestore(memento->scrollTop());
}
} // namespace Info::Polls

View File

@@ -0,0 +1,70 @@
/*
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 "info/info_content_widget.h"
#include "info/info_controller.h"
struct PeerListState;
namespace Info::Polls {
class InnerWidget;
class Memento final : public ContentMemento {
public:
Memento(not_null<PollData*> poll, FullMsgId contextId);
~Memento();
object_ptr<ContentWidget> createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) override;
Section section() const override;
void setListStates(base::flat_map<
QByteArray,
std::unique_ptr<PeerListState>> states);
auto listStates()
-> base::flat_map<QByteArray, std::unique_ptr<PeerListState>>;
private:
base::flat_map<
QByteArray,
std::unique_ptr<PeerListState>> _listStates;
};
class Widget final : public ContentWidget {
public:
Widget(QWidget *parent, not_null<Controller*> controller);
[[nodiscard]] not_null<PollData*> poll() const;
[[nodiscard]] FullMsgId contextId() const;
bool showInternal(
not_null<ContentMemento*> memento) override;
void setInternalState(
const QRect &geometry,
not_null<Memento*> memento);
rpl::producer<QString> title() override;
private:
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
std::shared_ptr<ContentMemento> doCreateMemento() override;
not_null<InnerWidget*> _inner;
};
} // namespace Info::Polls

View File

@@ -0,0 +1,81 @@
/*
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
*/
using "ui/basic.style";
LevelShape {
icon: icon;
position: point;
}
levelStyle: TextStyle(defaultTextStyle) {
font: font(9px semibold);
}
levelTextFg: windowFgActive;
levelMargin: margins(4px, 3px, 2px, 2px);
levelBase: LevelShape {
position: point(0px, 6px);
}
level1: LevelShape(levelBase) {
position: point(-1px, 6px);
icon: icon {{ "levels/level1_inner-26x26", windowBgActive }};
}
level2: LevelShape(level1) {
icon: icon {{ "levels/level2_inner-26x26", windowBgActive }};
}
level3: LevelShape(level1) {
icon: icon {{ "levels/level3_inner-26x26", windowBgActive }};
}
level4: LevelShape(level1) {
icon: icon {{ "levels/level4_inner-26x26", windowBgActive }};
}
level5: LevelShape(level1) {
icon: icon {{ "levels/level5_inner-26x26", windowBgActive }};
}
level6: LevelShape(level1) {
icon: icon {{ "levels/level6_inner-26x26", windowBgActive }};
}
level7: LevelShape(level1) {
icon: icon {{ "levels/level7_inner-26x26", windowBgActive }};
}
level8: LevelShape(level1) {
icon: icon {{ "levels/level8_inner-26x26", windowBgActive }};
}
level9: LevelShape(level1) {
icon: icon {{ "levels/level9_inner-26x26", windowBgActive }};
}
level10: LevelShape(levelBase) {
icon: icon {{ "levels/level10_inner-26x26", windowBgActive }};
}
level20: LevelShape(levelBase) {
icon: icon {{ "levels/level20_inner-26x26", windowBgActive }};
}
level30: LevelShape(levelBase) {
icon: icon {{ "levels/level30_inner-26x26", windowBgActive }};
}
level40: LevelShape(levelBase) {
icon: icon {{ "levels/level40_inner-26x26", windowBgActive }};
}
level50: LevelShape(levelBase) {
icon: icon {{ "levels/level50_inner-26x26", windowBgActive }};
}
level60: LevelShape(levelBase) {
icon: icon {{ "levels/level60_inner-26x26", windowBgActive }};
}
level70: LevelShape(levelBase) {
icon: icon {{ "levels/level70_inner-26x26", windowBgActive }};
}
level80: LevelShape(levelBase) {
icon: icon {{ "levels/level80_inner-26x26", windowBgActive }};
}
level90: LevelShape(levelBase) {
icon: icon {{ "levels/level90_inner-26x26", windowBgActive }};
}
levelNegative: LevelShape(levelBase) {
icon: icon {{ "levels/level_warning-18x18", attentionButtonFg, point(6px, 5px) }};
}
levelNegativeBubble: icon {{ "levels/level_warning-28x28", windowFgActive }};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,81 @@
/*
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 "base/object_ptr.h"
namespace Ui {
class RpWidget;
class VerticalLayout;
class MultiSlideTracker;
} // namespace Ui
namespace Data {
class ForumTopic;
class SavedSublist;
} // namespace Data
namespace Info {
class Controller;
} // namespace Info
namespace Info::Profile {
extern const char kOptionShowPeerIdBelowAbout[];
extern const char kOptionShowChannelJoinedBelowAbout[];
class Cover;
struct Origin;
object_ptr<Ui::RpWidget> SetupDetails(
not_null<Controller*> controller,
not_null<Ui::RpWidget*> parent,
not_null<PeerData*> peer,
Origin origin,
Ui::MultiSlideTracker &mainTracker);
object_ptr<Ui::RpWidget> SetupDetails(
not_null<Controller*> controller,
not_null<Ui::RpWidget*> parent,
not_null<Data::ForumTopic*> topic,
Ui::MultiSlideTracker &mainTracker);
object_ptr<Ui::RpWidget> SetupDetails(
not_null<Controller*> controller,
not_null<Ui::RpWidget*> parent,
not_null<Data::SavedSublist*> sublist,
Ui::MultiSlideTracker &mainTracker);
object_ptr<Ui::RpWidget> SetupActions(
not_null<Controller*> controller,
not_null<Ui::RpWidget*> parent,
not_null<PeerData*> peer);
object_ptr<Ui::RpWidget> SetupChannelMembersAndManage(
not_null<Controller*> controller,
not_null<Ui::RpWidget*> parent,
not_null<PeerData*> peer);
Cover *AddCover(
not_null<Ui::VerticalLayout*> container,
not_null<Controller*> controller,
not_null<PeerData*> peer,
Data::ForumTopic *topic,
Data::SavedSublist *sublist);
void AddDetails(
not_null<Ui::VerticalLayout*> container,
not_null<Controller*> controller,
not_null<PeerData*> peer,
Data::ForumTopic *topic,
Data::SavedSublist *sublist,
Origin origin,
Ui::MultiSlideTracker &mainTracker,
rpl::variable<bool> &dividerOverridden);
} // namespace Info::Profile

View File

@@ -0,0 +1,299 @@
/*
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 "info/profile/info_profile_badge.h"
#include "data/data_changes.h"
#include "data/data_emoji_statuses.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "data/stickers/data_custom_emoji.h"
#include "info/profile/info_profile_values.h"
#include "info/profile/info_profile_emoji_status_panel.h"
#include "lang/lang_keys.h"
#include "ui/widgets/buttons.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "main/main_session.h"
#include "styles/style_info.h"
namespace Info::Profile {
namespace {
[[nodiscard]] bool HasPremiumClick(const Badge::Content &content) {
return content.badge == BadgeType::Premium
|| (content.badge == BadgeType::Verified && content.emojiStatusId);
}
} // namespace
Badge::Badge(
not_null<QWidget*> parent,
const style::InfoPeerBadge &st,
not_null<Main::Session*> session,
rpl::producer<Content> content,
EmojiStatusPanel *emojiStatusPanel,
Fn<bool()> animationPaused,
int customStatusLoopsLimit,
base::flags<BadgeType> allowed)
: _parent(parent)
, _st(st)
, _session(session)
, _emojiStatusPanel(emojiStatusPanel)
, _customStatusLoopsLimit(customStatusLoopsLimit)
, _allowed(allowed)
, _animationPaused(std::move(animationPaused)) {
std::move(
content
) | rpl::on_next([=](Content content) {
setContent(content);
}, _lifetime);
}
Badge::~Badge() = default;
Ui::RpWidget *Badge::widget() const {
return _view.data();
}
void Badge::setContent(Content content) {
if (!(_allowed & content.badge)
|| (!_session->premiumBadgesShown()
&& content.badge == BadgeType::Premium)) {
content.badge = BadgeType::None;
}
if (!(_allowed & content.badge)) {
content.badge = BadgeType::None;
}
if (_content == content) {
return;
}
_content = content;
_emojiStatus = nullptr;
_view.destroy();
if (_content.badge == BadgeType::None) {
_updated.fire({});
return;
}
_view.create(_parent);
_view->show();
switch (_content.badge) {
case BadgeType::Verified:
case BadgeType::BotVerified:
case BadgeType::Premium: {
const auto id = _content.emojiStatusId;
const auto emoji = id
? (Data::FrameSizeFromTag(sizeTag())
/ style::DevicePixelRatio())
: 0;
const auto &style = st();
const auto icon = (_content.badge == BadgeType::Verified)
? &style.verified
: id
? nullptr
: &style.premium;
const auto iconForeground = (_content.badge == BadgeType::Verified)
? &style.verifiedCheck
: nullptr;
if (id) {
_emojiStatus = _session->data().customEmojiManager().create(
Data::EmojiStatusCustomId(id),
[raw = _view.data()] { raw->update(); },
sizeTag());
if (_content.badge == BadgeType::BotVerified) {
_emojiStatus = std::make_unique<Ui::Text::FirstFrameEmoji>(
std::move(_emojiStatus));
} else if (_customStatusLoopsLimit > 0) {
_emojiStatus = std::make_unique<Ui::Text::LimitedLoopsEmoji>(
std::move(_emojiStatus),
_customStatusLoopsLimit);
}
}
const auto width = emoji + (icon ? icon->width() : 0);
const auto height = std::max(emoji, icon ? icon->height() : 0);
_view->resize(width, height);
_view->paintRequest(
) | rpl::on_next([=, check = _view.data()]{
if (_emojiStatus) {
auto args = Ui::Text::CustomEmoji::Context{
.textColor = style.premiumFg->c,
.now = crl::now(),
.paused = ((_animationPaused && _animationPaused())
|| On(PowerSaving::kEmojiStatus)),
};
if (!_emojiStatusPanel
|| !_emojiStatusPanel->paintBadgeFrame(check)) {
Painter p(check);
_emojiStatus->paint(p, args);
}
}
if (icon) {
auto p = Painter(check);
if (_overrideSt && !iconForeground) {
icon->paint(
p,
emoji,
0,
check->width(),
_overrideSt->premiumFg->c);
} else {
icon->paint(p, emoji, 0, check->width());
}
if (iconForeground) {
if (_overrideSt) {
iconForeground->paint(
p,
emoji,
0,
check->width(),
_overrideSt->premiumFg->c);
} else {
iconForeground->paint(p, emoji, 0, check->width());
}
}
}
}, _view->lifetime());
} break;
case BadgeType::Scam:
case BadgeType::Fake:
case BadgeType::Direct: {
const auto type = (_content.badge == BadgeType::Direct)
? Ui::TextBadgeType::Direct
: (_content.badge == BadgeType::Fake)
? Ui::TextBadgeType::Fake
: Ui::TextBadgeType::Scam;
const auto size = Ui::TextBadgeSize(type);
const auto skip = st::infoVerifiedCheckPosition.x();
_view->resize(
size.width() + 2 * skip,
size.height() + 2 * skip);
_view->paintRequest(
) | rpl::on_next([=, badge = _view.data()]{
Painter p(badge);
Ui::DrawTextBadge(
type,
p,
badge->rect().marginsRemoved({ skip, skip, skip, skip }),
badge->width(),
(type == Ui::TextBadgeType::Direct
? st::windowSubTextFg
: st::attentionButtonFg));
}, _view->lifetime());
} break;
}
if (!HasPremiumClick(_content) || !_premiumClickCallback) {
_view->setAttribute(Qt::WA_TransparentForMouseEvents);
} else {
_view->setClickedCallback(_premiumClickCallback);
}
_updated.fire({});
}
void Badge::setPremiumClickCallback(Fn<void()> callback) {
_premiumClickCallback = std::move(callback);
if (_view && HasPremiumClick(_content)) {
if (!_premiumClickCallback) {
_view->setAttribute(Qt::WA_TransparentForMouseEvents);
} else {
_view->setAttribute(Qt::WA_TransparentForMouseEvents, false);
_view->setClickedCallback(_premiumClickCallback);
}
}
}
void Badge::setOverrideStyle(const style::InfoPeerBadge *st) {
const auto was = _content;
_overrideSt = st;
_content = {};
setContent(was);
}
rpl::producer<> Badge::updated() const {
return _updated.events();
}
void Badge::move(int left, int top, int bottom) {
if (!_view) {
return;
}
const auto &style = st();
const auto star = !_emojiStatus
&& (_content.badge == BadgeType::Premium
|| _content.badge == BadgeType::Verified);
const auto fake = !_emojiStatus && !star;
const auto skip = fake ? 0 : style.position.x();
const auto badgeLeft = left + skip;
const auto badgeTop = top
+ (star
? style.position.y()
: (bottom - top - _view->height()) / 2);
_view->moveToLeft(badgeLeft, badgeTop);
}
const style::InfoPeerBadge &Badge::st() const {
return _overrideSt ? *_overrideSt : _st;
}
Data::CustomEmojiSizeTag Badge::sizeTag() const {
using SizeTag = Data::CustomEmojiSizeTag;
const auto &style = st();
return (style.sizeTag == 2)
? SizeTag::Isolated
: (style.sizeTag == 1)
? SizeTag::Large
: SizeTag::Normal;
}
rpl::producer<Badge::Content> BadgeContentForPeer(not_null<PeerData*> peer) {
const auto statusOnlyForPremium = peer->isUser();
return rpl::combine(
BadgeValue(peer),
EmojiStatusIdValue(peer)
) | rpl::map([=](BadgeType badge, EmojiStatusId emojiStatusId) {
if (emojiStatusId.collectible) {
return Badge::Content{ BadgeType::Premium, emojiStatusId };
}
if (badge == BadgeType::Verified) {
badge = BadgeType::None;
}
if (statusOnlyForPremium && badge != BadgeType::Premium) {
emojiStatusId = EmojiStatusId();
} else if (emojiStatusId && badge == BadgeType::None) {
badge = BadgeType::Premium;
}
return Badge::Content{ badge, emojiStatusId };
});
}
rpl::producer<Badge::Content> VerifiedContentForPeer(
not_null<PeerData*> peer) {
return BadgeValue(peer) | rpl::map([=](BadgeType badge) {
if (badge != BadgeType::Verified) {
badge = BadgeType::None;
}
return Badge::Content{ badge };
});
}
rpl::producer<Badge::Content> BotVerifyBadgeForPeer(
not_null<PeerData*> peer) {
return peer->session().changes().peerFlagsValue(
peer,
Data::PeerUpdate::Flag::VerifyInfo
) | rpl::map([=] {
const auto info = peer->botVerifyDetails();
return Badge::Content{
.badge = info ? BadgeType::BotVerified : BadgeType::None,
.emojiStatusId = { info ? info->iconId : DocumentId() },
};
});
}
} // namespace Info::Profile

View File

@@ -0,0 +1,107 @@
/*
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 "base/flags.h"
#include "base/object_ptr.h"
namespace style {
struct InfoPeerBadge;
} // namespace style
namespace Data {
enum class CustomEmojiSizeTag : uchar;
} // namespace Data
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class RpWidget;
class AbstractButton;
} // namespace Ui
namespace Ui::Text {
class CustomEmoji;
} // namespace Ui::Text
namespace Info::Profile {
class EmojiStatusPanel;
enum class BadgeType : uchar {
None = 0x00,
Verified = 0x01,
BotVerified = 0x02,
Premium = 0x04,
Scam = 0x08,
Fake = 0x10,
Direct = 0x20,
};
inline constexpr bool is_flag_type(BadgeType) { return true; }
class Badge final {
public:
struct Content {
BadgeType badge = BadgeType::None;
EmojiStatusId emojiStatusId;
friend inline bool operator==(Content, Content) = default;
};
Badge(
not_null<QWidget*> parent,
const style::InfoPeerBadge &st,
not_null<Main::Session*> session,
rpl::producer<Content> content,
EmojiStatusPanel *emojiStatusPanel,
Fn<bool()> animationPaused,
int customStatusLoopsLimit = 0,
base::flags<BadgeType> allowed
= base::flags<BadgeType>::from_raw(-1));
~Badge();
[[nodiscard]] Ui::RpWidget *widget() const;
void setPremiumClickCallback(Fn<void()> callback);
void setOverrideStyle(const style::InfoPeerBadge *st);
[[nodiscard]] rpl::producer<> updated() const;
void move(int left, int top, int bottom);
[[nodiscard]] Data::CustomEmojiSizeTag sizeTag() const;
private:
void setContent(Content content);
[[nodiscard]] const style::InfoPeerBadge &st() const;
const not_null<QWidget*> _parent;
const style::InfoPeerBadge &_st;
const style::InfoPeerBadge *_overrideSt = nullptr;
const not_null<Main::Session*> _session;
EmojiStatusPanel *_emojiStatusPanel = nullptr;
const int _customStatusLoopsLimit = 0;
std::unique_ptr<Ui::Text::CustomEmoji> _emojiStatus;
base::flags<BadgeType> _allowed;
Content _content;
Fn<void()> _premiumClickCallback;
Fn<bool()> _animationPaused;
object_ptr<Ui::AbstractButton> _view = { nullptr };
rpl::event_stream<> _updated;
rpl::lifetime _lifetime;
};
[[nodiscard]] rpl::producer<Badge::Content> BadgeContentForPeer(
not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<Badge::Content> VerifiedContentForPeer(
not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<Badge::Content> BotVerifyBadgeForPeer(
not_null<PeerData*> peer);
} // namespace Info::Profile

Some files were not shown because too many files have changed in this diff Show More