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
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:
477
Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp
Normal file
477
Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.cpp
Normal 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
|
||||
|
||||
67
Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.h
Normal file
67
Telegram/SourceFiles/info/bot/earn/info_bot_earn_list.h
Normal 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
|
||||
118
Telegram/SourceFiles/info/bot/earn/info_bot_earn_widget.cpp
Normal file
118
Telegram/SourceFiles/info/bot/earn/info_bot_earn_widget.cpp
Normal 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
|
||||
68
Telegram/SourceFiles/info/bot/earn/info_bot_earn_widget.h
Normal file
68
Telegram/SourceFiles/info/bot/earn/info_bot_earn_widget.h
Normal 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
|
||||
1036
Telegram/SourceFiles/info/bot/starref/info_bot_starref_common.cpp
Normal file
1036
Telegram/SourceFiles/info/bot/starref/info_bot_starref_common.cpp
Normal file
File diff suppressed because it is too large
Load Diff
112
Telegram/SourceFiles/info/bot/starref/info_bot_starref_common.h
Normal file
112
Telegram/SourceFiles/info/bot/starref/info_bot_starref_common.h
Normal 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
@@ -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
@@ -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
@@ -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);
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
186
Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.cpp
Normal file
186
Telegram/SourceFiles/info/channel_statistics/earn/earn_icons.cpp
Normal 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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
576
Telegram/SourceFiles/info/downloads/info_downloads_provider.cpp
Normal file
576
Telegram/SourceFiles/info/downloads/info_downloads_provider.cpp
Normal 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 §ion = 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
|
||||
155
Telegram/SourceFiles/info/downloads/info_downloads_provider.h
Normal file
155
Telegram/SourceFiles/info/downloads/info_downloads_provider.h
Normal 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
|
||||
144
Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp
Normal file
144
Telegram/SourceFiles/info/downloads/info_downloads_widget.cpp
Normal 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
|
||||
|
||||
76
Telegram/SourceFiles/info/downloads/info_downloads_widget.h
Normal file
76
Telegram/SourceFiles/info/downloads/info_downloads_widget.h
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 §ion = 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
1450
Telegram/SourceFiles/info/info.style
Normal file
1450
Telegram/SourceFiles/info/info.style
Normal file
File diff suppressed because it is too large
Load Diff
635
Telegram/SourceFiles/info/info_content_widget.cpp
Normal file
635
Telegram/SourceFiles/info/info_content_widget.cpp
Normal 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
|
||||
386
Telegram/SourceFiles/info/info_content_widget.h
Normal file
386
Telegram/SourceFiles/info/info_content_widget.h
Normal 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
|
||||
563
Telegram/SourceFiles/info/info_controller.cpp
Normal file
563
Telegram/SourceFiles/info/info_controller.cpp
Normal 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 ¶ms) {
|
||||
return parentController()->showSection(std::move(memento), params);
|
||||
}
|
||||
|
||||
void AbstractController::showBackFromStack(
|
||||
const Window::SectionShow ¶ms) {
|
||||
return parentController()->showBackFromStack(params);
|
||||
}
|
||||
|
||||
void AbstractController::showPeerHistory(
|
||||
PeerId peerId,
|
||||
const Window::SectionShow ¶ms,
|
||||
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 ¶ms) {
|
||||
if (!_widget->showInternal(memento.get(), params)) {
|
||||
AbstractController::showSection(std::move(memento), params);
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::showBackFromStack(const Window::SectionShow ¶ms) {
|
||||
if (!_widget->showBackFromStackInternal(params)) {
|
||||
AbstractController::showBackFromStack(params);
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::removeFromStack(const std::vector<Section> §ions) 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
|
||||
394
Telegram/SourceFiles/info/info_controller.h
Normal file
394
Telegram/SourceFiles/info/info_controller.h
Normal 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 ¶ms = Window::SectionShow()) override;
|
||||
void showBackFromStack(
|
||||
const Window::SectionShow ¶ms = Window::SectionShow()) override;
|
||||
|
||||
void showPeerHistory(
|
||||
PeerId peerId,
|
||||
const Window::SectionShow ¶ms = 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 ¶ms = Window::SectionShow()) override;
|
||||
void showBackFromStack(
|
||||
const Window::SectionShow ¶ms = Window::SectionShow()) override;
|
||||
|
||||
void removeFromStack(const std::vector<Section> §ions) 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
|
||||
361
Telegram/SourceFiles/info/info_flexible_scroll.cpp
Normal file
361
Telegram/SourceFiles/info/info_flexible_scroll.cpp
Normal 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
|
||||
58
Telegram/SourceFiles/info/info_flexible_scroll.h
Normal file
58
Telegram/SourceFiles/info/info_flexible_scroll.h
Normal 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
|
||||
387
Telegram/SourceFiles/info/info_layer_widget.cpp
Normal file
387
Telegram/SourceFiles/info/info_layer_widget.cpp
Normal 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 ¶ms) {
|
||||
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
|
||||
93
Telegram/SourceFiles/info/info_layer_widget.h
Normal file
93
Telegram/SourceFiles/info/info_layer_widget.h
Normal 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 ¶ms) 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
|
||||
327
Telegram/SourceFiles/info/info_memento.cpp
Normal file
327
Telegram/SourceFiles/info/info_memento.cpp
Normal 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
|
||||
157
Telegram/SourceFiles/info/info_memento.h
Normal file
157
Telegram/SourceFiles/info/info_memento.h
Normal 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
|
||||
143
Telegram/SourceFiles/info/info_section_widget.cpp
Normal file
143
Telegram/SourceFiles/info/info_section_widget.cpp
Normal 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 ¶ms) {
|
||||
return _content->grabForShowAnimation(params);
|
||||
}
|
||||
|
||||
void SectionWidget::doSetInnerFocus() {
|
||||
_content->setInnerFocus();
|
||||
}
|
||||
|
||||
void SectionWidget::showFinishedHook() {
|
||||
_topBarSurrogate.destroy();
|
||||
_content->showFast();
|
||||
}
|
||||
|
||||
void SectionWidget::showAnimatedHook(
|
||||
const Window::SectionSlideParams ¶ms) {
|
||||
_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 ¶ms) {
|
||||
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
|
||||
75
Telegram/SourceFiles/info/info_section_widget.h
Normal file
75
Telegram/SourceFiles/info/info_section_widget.h
Normal 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 ¶ms) override;
|
||||
|
||||
bool showInternal(
|
||||
not_null<Window::SectionMemento*> memento,
|
||||
const Window::SectionShow ¶ms) 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 ¶ms) 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
|
||||
816
Telegram/SourceFiles/info/info_top_bar.cpp
Normal file
816
Telegram/SourceFiles/info/info_top_bar.cpp
Normal 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
|
||||
200
Telegram/SourceFiles/info/info_top_bar.h
Normal file
200
Telegram/SourceFiles/info/info_top_bar.h
Normal 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
|
||||
1091
Telegram/SourceFiles/info/info_wrap_widget.cpp
Normal file
1091
Telegram/SourceFiles/info/info_wrap_widget.cpp
Normal file
File diff suppressed because it is too large
Load Diff
251
Telegram/SourceFiles/info/info_wrap_widget.h
Normal file
251
Telegram/SourceFiles/info/info_wrap_widget.h
Normal 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 ¶ms) override;
|
||||
|
||||
void forceContentRepaint();
|
||||
|
||||
bool showInternal(
|
||||
not_null<Window::SectionMemento*> memento,
|
||||
const Window::SectionShow ¶ms) override;
|
||||
bool showBackFromStackInternal(const Window::SectionShow ¶ms);
|
||||
void removeFromStack(const std::vector<Section> §ions);
|
||||
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 ¶ms) 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 ¶ms);
|
||||
bool returnToFirstStackFrame(
|
||||
not_null<ContentMemento*> memento,
|
||||
const Window::SectionShow ¶ms);
|
||||
void setupTop();
|
||||
void setupTopBarMenuToggle();
|
||||
void createTopBar();
|
||||
void highlightTopBar();
|
||||
void setupShortcuts();
|
||||
|
||||
[[nodiscard]] bool willHaveBackButton(
|
||||
const Window::SectionShow ¶ms) 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
|
||||
399
Telegram/SourceFiles/info/media/info_media_buttons.cpp
Normal file
399
Telegram/SourceFiles/info/media/info_media_buttons.cpp
Normal 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
|
||||
80
Telegram/SourceFiles/info/media/info_media_buttons.h
Normal file
80
Telegram/SourceFiles/info/media/info_media_buttons.h
Normal 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
|
||||
94
Telegram/SourceFiles/info/media/info_media_common.cpp
Normal file
94
Telegram/SourceFiles/info/media/info_media_common.cpp
Normal 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
|
||||
185
Telegram/SourceFiles/info/media/info_media_common.h
Normal file
185
Telegram/SourceFiles/info/media/info_media_common.h
Normal 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
|
||||
106
Telegram/SourceFiles/info/media/info_media_empty_widget.cpp
Normal file
106
Telegram/SourceFiles/info/media/info_media_empty_widget.cpp
Normal 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
|
||||
42
Telegram/SourceFiles/info/media/info_media_empty_widget.h
Normal file
42
Telegram/SourceFiles/info/media/info_media_empty_widget.h
Normal 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
|
||||
249
Telegram/SourceFiles/info/media/info_media_inner_widget.cpp
Normal file
249
Telegram/SourceFiles/info/media/info_media_inner_widget.cpp
Normal 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
|
||||
88
Telegram/SourceFiles/info/media/info_media_inner_widget.h
Normal file
88
Telegram/SourceFiles/info/media/info_media_inner_widget.h
Normal 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
|
||||
462
Telegram/SourceFiles/info/media/info_media_list_section.cpp
Normal file
462
Telegram/SourceFiles/info/media/info_media_list_section.cpp
Normal 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
|
||||
100
Telegram/SourceFiles/info/media/info_media_list_section.h
Normal file
100
Telegram/SourceFiles/info/media/info_media_list_section.h
Normal 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
|
||||
2717
Telegram/SourceFiles/info/media/info_media_list_widget.cpp
Normal file
2717
Telegram/SourceFiles/info/media/info_media_list_widget.cpp
Normal file
File diff suppressed because it is too large
Load Diff
382
Telegram/SourceFiles/info/media/info_media_list_widget.h
Normal file
382
Telegram/SourceFiles/info/media/info_media_list_widget.h
Normal 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 §ion) 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
|
||||
578
Telegram/SourceFiles/info/media/info_media_provider.cpp
Normal file
578
Telegram/SourceFiles/info/media/info_media_provider.cpp
Normal 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
|
||||
125
Telegram/SourceFiles/info/media/info_media_provider.h
Normal file
125
Telegram/SourceFiles/info/media/info_media_provider.h
Normal 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
|
||||
201
Telegram/SourceFiles/info/media/info_media_widget.cpp
Normal file
201
Telegram/SourceFiles/info/media/info_media_widget.cpp
Normal 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
|
||||
140
Telegram/SourceFiles/info/media/info_media_widget.h
Normal file
140
Telegram/SourceFiles/info/media/info_media_widget.h
Normal 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
|
||||
111
Telegram/SourceFiles/info/members/info_members_widget.cpp
Normal file
111
Telegram/SourceFiles/info/members/info_members_widget.cpp
Normal 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
|
||||
73
Telegram/SourceFiles/info/members/info_members_widget.h
Normal file
73
Telegram/SourceFiles/info/members/info_members_widget.h
Normal 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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
1402
Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp
Normal file
1402
Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.cpp
Normal file
File diff suppressed because it is too large
Load Diff
308
Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h
Normal file
308
Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_common.h
Normal 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
|
||||
2669
Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp
Normal file
2669
Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.cpp
Normal file
File diff suppressed because it is too large
Load Diff
127
Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.h
Normal file
127
Telegram/SourceFiles/info/peer_gifts/info_peer_gifts_widget.h
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
115
Telegram/SourceFiles/info/polls/info_polls_results_widget.cpp
Normal file
115
Telegram/SourceFiles/info/polls/info_polls_results_widget.cpp
Normal 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
|
||||
70
Telegram/SourceFiles/info/polls/info_polls_results_widget.h
Normal file
70
Telegram/SourceFiles/info/polls/info_polls_results_widget.h
Normal 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
|
||||
81
Telegram/SourceFiles/info/profile/info_levels.style
Normal file
81
Telegram/SourceFiles/info/profile/info_levels.style
Normal 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 }};
|
||||
3148
Telegram/SourceFiles/info/profile/info_profile_actions.cpp
Normal file
3148
Telegram/SourceFiles/info/profile/info_profile_actions.cpp
Normal file
File diff suppressed because it is too large
Load Diff
81
Telegram/SourceFiles/info/profile/info_profile_actions.h
Normal file
81
Telegram/SourceFiles/info/profile/info_profile_actions.h
Normal 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> ÷rOverridden);
|
||||
|
||||
} // namespace Info::Profile
|
||||
|
||||
299
Telegram/SourceFiles/info/profile/info_profile_badge.cpp
Normal file
299
Telegram/SourceFiles/info/profile/info_profile_badge.cpp
Normal 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
|
||||
107
Telegram/SourceFiles/info/profile/info_profile_badge.h
Normal file
107
Telegram/SourceFiles/info/profile/info_profile_badge.h
Normal 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
Reference in New Issue
Block a user