mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-15 21:57:10 +02:00
Implement folder link add / join design.
This commit is contained in:
parent
7684dbc701
commit
0faadc8fa0
10 changed files with 725 additions and 113 deletions
|
@ -15,11 +15,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "data/data_session.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "settings/settings_common.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/controls/filter_link_header.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/toasts/common_toasts.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_filter_icons.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_settings.h"
|
||||
|
||||
|
@ -31,19 +34,6 @@ enum class ToggleAction {
|
|||
Removing,
|
||||
};
|
||||
|
||||
enum class HeaderType {
|
||||
AddingFilter,
|
||||
AddingChats,
|
||||
AllAdded,
|
||||
Removing,
|
||||
};
|
||||
|
||||
struct HeaderDescriptor {
|
||||
base::required<HeaderType> type;
|
||||
base::required<QString> title;
|
||||
int badge = 0;
|
||||
};
|
||||
|
||||
class ToggleChatsController final
|
||||
: public PeerListController
|
||||
, public base::has_weak_ptr {
|
||||
|
@ -63,11 +53,14 @@ public:
|
|||
[[nodiscard]] auto selectedValue() const
|
||||
-> rpl::producer<base::flat_set<not_null<PeerData*>>>;
|
||||
|
||||
void setAddedTopHeight(int addedTopHeight);
|
||||
|
||||
private:
|
||||
void setupAboveWidget();
|
||||
void setupBelowWidget();
|
||||
|
||||
const not_null<Window::SessionController*> _window;
|
||||
Ui::RpWidget *_addedTopWidget = nullptr;
|
||||
|
||||
ToggleAction _action = ToggleAction::Adding;
|
||||
QString _slug;
|
||||
|
@ -82,44 +75,42 @@ private:
|
|||
|
||||
};
|
||||
|
||||
[[nodiscard]] rpl::producer<QString> TitleText(HeaderType type) {
|
||||
[[nodiscard]] tr::phrase<> TitleText(Ui::FilterLinkHeaderType type) {
|
||||
using Type = Ui::FilterLinkHeaderType;
|
||||
switch (type) {
|
||||
case HeaderType::AddingFilter:
|
||||
return tr::lng_filters_by_link_title();
|
||||
case HeaderType::AddingChats:
|
||||
return tr::lng_filters_by_link_more();
|
||||
case HeaderType::AllAdded:
|
||||
return tr::lng_filters_by_link_already();
|
||||
case HeaderType::Removing:
|
||||
return tr::lng_filters_by_link_remove();
|
||||
case Type::AddingFilter: return tr::lng_filters_by_link_title;
|
||||
case Type::AddingChats: return tr::lng_filters_by_link_more;
|
||||
case Type::AllAdded: return tr::lng_filters_by_link_already;
|
||||
case Type::Removing: return tr::lng_filters_by_link_remove;
|
||||
}
|
||||
Unexpected("HeaderType in TitleText.");
|
||||
Unexpected("Ui::FilterLinkHeaderType in TitleText.");
|
||||
}
|
||||
|
||||
void FillHeader(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
HeaderDescriptor descriptor) {
|
||||
const auto phrase = (descriptor.type == HeaderType::AddingFilter)
|
||||
[[nodiscard]] TextWithEntities AboutText(
|
||||
Ui::FilterLinkHeaderType type,
|
||||
const QString &title) {
|
||||
using Type = Ui::FilterLinkHeaderType;
|
||||
const auto phrase = (type == Type::AddingFilter)
|
||||
? tr::lng_filters_by_link_sure
|
||||
: (descriptor.type == HeaderType::AddingChats)
|
||||
: (type == Type::AddingChats)
|
||||
? tr::lng_filters_by_link_more_sure
|
||||
: (descriptor.type == HeaderType::AllAdded)
|
||||
: (type == Type::AllAdded)
|
||||
? tr::lng_filters_by_link_already_about
|
||||
: tr::lng_filters_by_link_remove_sure;
|
||||
auto boldTitle = Ui::Text::Bold(descriptor.title);
|
||||
auto description = (descriptor.type == HeaderType::AddingFilter)
|
||||
auto boldTitle = Ui::Text::Bold(title);
|
||||
return (type == Type::AddingFilter)
|
||||
? tr::lng_filters_by_link_sure(
|
||||
tr::now,
|
||||
lt_folder,
|
||||
std::move(boldTitle),
|
||||
Ui::Text::WithEntities)
|
||||
: (descriptor.type == HeaderType::AddingChats)
|
||||
: (type == Type::AddingChats)
|
||||
? tr::lng_filters_by_link_more_sure(
|
||||
tr::now,
|
||||
lt_folder,
|
||||
std::move(boldTitle),
|
||||
Ui::Text::WithEntities)
|
||||
: (descriptor.type == HeaderType::AllAdded)
|
||||
: (type == Type::AllAdded)
|
||||
? tr::lng_filters_by_link_already_about(
|
||||
tr::now,
|
||||
lt_folder,
|
||||
|
@ -130,35 +121,92 @@ void FillHeader(
|
|||
lt_folder,
|
||||
std::move(boldTitle),
|
||||
Ui::Text::WithEntities);
|
||||
container->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
container,
|
||||
phrase(
|
||||
lt_folder,
|
||||
rpl::single(Ui::Text::Bold(descriptor.title)),
|
||||
Ui::Text::WithEntities),
|
||||
st::boxDividerLabel),
|
||||
st::boxRowPadding);
|
||||
}
|
||||
|
||||
void InitFilterLinkHeader(
|
||||
not_null<PeerListBox*> box,
|
||||
Fn<void(int)> setAddedTopHeight,
|
||||
Ui::FilterLinkHeaderType type,
|
||||
const QString &title,
|
||||
rpl::producer<int> count) {
|
||||
auto header = Ui::MakeFilterLinkHeader(box, {
|
||||
.type = type,
|
||||
.title = TitleText(type)(tr::now),
|
||||
.about = AboutText(type, title),
|
||||
.folderTitle = title,
|
||||
.folderIcon = &st::foldersCustomActive,
|
||||
.badge = (type == Ui::FilterLinkHeaderType::AddingChats
|
||||
? std::move(count)
|
||||
: rpl::single(0)),
|
||||
});
|
||||
const auto widget = header.widget;
|
||||
widget->resizeToWidth(st::boxWideWidth);
|
||||
Ui::SendPendingMoveResizeEvents(widget);
|
||||
|
||||
const auto min = widget->minimumHeight(), max = widget->maximumHeight();
|
||||
widget->resize(st::boxWideWidth, max);
|
||||
|
||||
box->setAddedTopScrollSkip(max);
|
||||
std::move(
|
||||
header.wheelEvents
|
||||
) | rpl::start_with_next([=](not_null<QWheelEvent*> e) {
|
||||
box->sendScrollViewportEvent(e);
|
||||
}, widget->lifetime());
|
||||
|
||||
struct State {
|
||||
bool processing = false;
|
||||
int addedTopHeight = 0;
|
||||
};
|
||||
const auto state = widget->lifetime().make_state<State>();
|
||||
|
||||
box->scrolls(
|
||||
) | rpl::filter([=] {
|
||||
return !state->processing;
|
||||
}) | rpl::start_with_next([=] {
|
||||
state->processing = true;
|
||||
const auto guard = gsl::finally([&] { state->processing = false; });
|
||||
|
||||
const auto top = box->scrollTop();
|
||||
const auto height = box->scrollHeight();
|
||||
const auto headerHeight = std::max(max - top, min);
|
||||
const auto addedTopHeight = max - headerHeight;
|
||||
widget->resize(widget->width(), headerHeight);
|
||||
if (state->addedTopHeight < addedTopHeight) {
|
||||
setAddedTopHeight(addedTopHeight);
|
||||
box->setAddedTopScrollSkip(headerHeight);
|
||||
} else {
|
||||
box->setAddedTopScrollSkip(headerHeight);
|
||||
setAddedTopHeight(addedTopHeight);
|
||||
}
|
||||
state->addedTopHeight = addedTopHeight;
|
||||
box->peerListRefreshRows();
|
||||
}, widget->lifetime());
|
||||
|
||||
box->setNoContentMargin(true);
|
||||
}
|
||||
|
||||
void ImportInvite(
|
||||
base::weak_ptr<Window::SessionController> weak,
|
||||
const QString &slug,
|
||||
const base::flat_set<not_null<PeerData*>> &peers) {
|
||||
const base::flat_set<not_null<PeerData*>> &peers,
|
||||
Fn<void()> done,
|
||||
Fn<void()> fail) {
|
||||
Expects(!peers.empty());
|
||||
|
||||
const auto peer = peers.front();
|
||||
const auto api = &peer->session().api();
|
||||
const auto callback = [=](const MTPUpdates &result) {
|
||||
api->applyUpdates(result);
|
||||
done();
|
||||
};
|
||||
const auto error = [=](const MTP::Error &error) {
|
||||
if (const auto strong = weak.get()) {
|
||||
Ui::ShowMultilineToast({
|
||||
.parentOverride = Window::Show(strong).toastParent(),
|
||||
.text = { error.description() },
|
||||
.text = { error.type() },
|
||||
});
|
||||
}
|
||||
fail();
|
||||
};
|
||||
auto inputs = peers | ranges::views::transform([](auto peer) {
|
||||
return MTPInputPeer(peer->input);
|
||||
|
@ -182,6 +230,7 @@ ToggleChatsController::ToggleChatsController(
|
|||
, _filterId(filterId)
|
||||
, _filterTitle(title)
|
||||
, _chats(std::move(chats)) {
|
||||
setStyleOverrides(&st::filterLinkChatsList);
|
||||
}
|
||||
|
||||
void ToggleChatsController::prepare() {
|
||||
|
@ -218,19 +267,14 @@ void ToggleChatsController::setupAboveWidget() {
|
|||
auto wrap = object_ptr<Ui::VerticalLayout>((QWidget*)nullptr);
|
||||
const auto container = wrap.data();
|
||||
|
||||
const auto type = !_filterId
|
||||
? HeaderType::AddingFilter
|
||||
: (_action == ToggleAction::Adding)
|
||||
? HeaderType::AddingChats
|
||||
: HeaderType::Removing;
|
||||
delegate()->peerListSetTitle(TitleText(type));
|
||||
FillHeader(container, {
|
||||
.type = type,
|
||||
.title = _filterTitle,
|
||||
.badge = (type == HeaderType::AddingChats) ? int(_chats.size()) : 0,
|
||||
});
|
||||
|
||||
// lng_filters_by_link_join; // langs
|
||||
_addedTopWidget = container->add(object_ptr<Ui::RpWidget>(container));
|
||||
AddDivider(container);
|
||||
AddSubsectionTitle(
|
||||
container,
|
||||
tr::lng_filters_by_link_join(
|
||||
lt_count,
|
||||
rpl::single(float64(_chats.size()))),
|
||||
st::filterLinkSubsectionTitlePadding);
|
||||
|
||||
delegate()->peerListSetAboveWidget(std::move(wrap));
|
||||
}
|
||||
|
@ -241,7 +285,7 @@ void ToggleChatsController::setupBelowWidget() {
|
|||
(QWidget*)nullptr,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
(QWidget*)nullptr,
|
||||
tr::lng_filters_by_link_about(),
|
||||
tr::lng_filters_by_link_about(tr::now),
|
||||
st::boxDividerLabel),
|
||||
st::settingsDividerLabelPadding));
|
||||
}
|
||||
|
@ -255,17 +299,10 @@ auto ToggleChatsController::selectedValue() const
|
|||
return _selected.value();
|
||||
}
|
||||
|
||||
[[nodiscard]] void AlreadyFilterBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
const QString &title) {
|
||||
box->setTitle(TitleText(HeaderType::AllAdded));
|
||||
void ToggleChatsController::setAddedTopHeight(int addedTopHeight) {
|
||||
Expects(addedTopHeight >= 0);
|
||||
|
||||
FillHeader(box->verticalLayout(), {
|
||||
.type = HeaderType::AllAdded,
|
||||
.title = title,
|
||||
});
|
||||
|
||||
box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); });
|
||||
_addedTopWidget->resize(_addedTopWidget->width(), addedTopHeight);
|
||||
}
|
||||
|
||||
void ProcessFilterInvite(
|
||||
|
@ -279,15 +316,11 @@ void ProcessFilterInvite(
|
|||
return;
|
||||
}
|
||||
Core::App().hideMediaView();
|
||||
if (peers.empty()) {
|
||||
if (filterId) {
|
||||
strong->show(Box(AlreadyFilterBox, title));
|
||||
} else {
|
||||
Ui::ShowMultilineToast({
|
||||
.parentOverride = Window::Show(strong).toastParent(),
|
||||
.text = { tr::lng_group_invite_bad_link(tr::now) },
|
||||
});
|
||||
}
|
||||
if (peers.empty() && !filterId) {
|
||||
Ui::ShowMultilineToast({
|
||||
.parentOverride = Window::Show(strong).toastParent(),
|
||||
.text = { tr::lng_group_invite_bad_link(tr::now) },
|
||||
});
|
||||
return;
|
||||
}
|
||||
auto controller = std::make_unique<ToggleChatsController>(
|
||||
|
@ -298,42 +331,61 @@ void ProcessFilterInvite(
|
|||
title,
|
||||
std::move(peers));
|
||||
const auto raw = controller.get();
|
||||
auto initBox = [=](not_null<Ui::BoxContent*> box) {
|
||||
auto initBox = [=](not_null<PeerListBox*> box) {
|
||||
box->setStyle(st::filterInviteBox);
|
||||
|
||||
using Type = Ui::FilterLinkHeaderType;
|
||||
const auto type = !filterId
|
||||
? Type::AddingFilter
|
||||
: Type::AddingChats;
|
||||
auto badge = raw->selectedValue(
|
||||
) | rpl::map([=](const base::flat_set<not_null<PeerData*>> &peers) {
|
||||
return int(peers.size());
|
||||
});
|
||||
InitFilterLinkHeader(box, [=](int addedTopHeight) {
|
||||
raw->setAddedTopHeight(addedTopHeight);
|
||||
}, type, title, rpl::duplicate(badge));
|
||||
|
||||
auto owned = Ui::FilterLinkProcessButton(
|
||||
box,
|
||||
type,
|
||||
title,
|
||||
std::move(badge));
|
||||
|
||||
const auto button = owned.data();
|
||||
box->widthValue(
|
||||
) | rpl::start_with_next([=](int width) {
|
||||
const auto &padding = st::filterInviteBox.buttonPadding;
|
||||
button->resizeToWidth(width
|
||||
- padding.left()
|
||||
- padding.right());
|
||||
button->moveToLeft(padding.left(), padding.top());
|
||||
}, button->lifetime());
|
||||
|
||||
box->addButton(std::move(owned));
|
||||
|
||||
struct State {
|
||||
bool importing = false;
|
||||
};
|
||||
const auto state = box->lifetime().make_state<State>();
|
||||
|
||||
raw->selectedValue(
|
||||
) | rpl::start_with_next([=](
|
||||
base::flat_set<not_null<PeerData*>> &&peers) {
|
||||
const auto count = int(peers.size());
|
||||
|
||||
box->clearButtons();
|
||||
auto button = object_ptr<Ui::RoundButton>(
|
||||
box,
|
||||
rpl::single(count
|
||||
? u"Add %1 Chats"_q.arg(count)
|
||||
: u"Don't add chats"_q),
|
||||
st::defaultActiveButton);
|
||||
const auto raw = button.data();
|
||||
|
||||
box->widthValue(
|
||||
) | rpl::start_with_next([=](int width) {
|
||||
const auto &padding = st::filterInviteBox.buttonPadding;
|
||||
raw->resizeToWidth(width
|
||||
- padding.left()
|
||||
- padding.right());
|
||||
raw->moveToLeft(padding.left(), padding.top());
|
||||
}, raw->lifetime());
|
||||
|
||||
raw->setClickedCallback([=] {
|
||||
if (!count) {
|
||||
button->setClickedCallback([=] {
|
||||
if (peers.empty()) {
|
||||
box->closeBox();
|
||||
//} else if (count + alreadyInFilter() >= ...) {
|
||||
// #TODO filters
|
||||
} else {
|
||||
ImportInvite(weak, slug, peers);
|
||||
} else if (!state->importing) {
|
||||
state->importing = true;
|
||||
ImportInvite(weak, slug, peers, crl::guard(box, [=] {
|
||||
box->closeBox();
|
||||
}), crl::guard(box, [=] {
|
||||
state->importing = false;
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
box->addButton(std::move(button));
|
||||
}, box->lifetime());
|
||||
};
|
||||
strong->show(
|
||||
|
|
|
@ -531,7 +531,10 @@ void LinkController::addLinkBlock(not_null<Ui::VerticalLayout*> container) {
|
|||
&st::menuIconDelete);
|
||||
return result;
|
||||
};
|
||||
AddSubsectionTitle(container, tr::lng_filters_link_subtitle());
|
||||
AddSubsectionTitle(
|
||||
container,
|
||||
tr::lng_filters_link_subtitle(),
|
||||
st::filterLinkSubsectionTitlePadding);
|
||||
|
||||
const auto prefix = u"https://"_q;
|
||||
const auto label = container->lifetime().make_state<Ui::InviteLinkLabel>(
|
||||
|
@ -640,7 +643,8 @@ void LinkController::setupAboveWidget() {
|
|||
});
|
||||
Settings::AddSubsectionTitle(
|
||||
container,
|
||||
std::move(subtitle));
|
||||
std::move(subtitle),
|
||||
st::filterLinkSubsectionTitlePadding);
|
||||
|
||||
delegate()->peerListSetAboveWidget(std::move(wrap));
|
||||
}
|
||||
|
|
|
@ -140,6 +140,7 @@ void PeerListBox::createMultiSelect() {
|
|||
|
||||
void PeerListBox::setAddedTopScrollSkip(int skip) {
|
||||
_addedTopScrollSkip = skip;
|
||||
_scrollBottomFixed = false;
|
||||
updateScrollSkips();
|
||||
}
|
||||
|
||||
|
|
|
@ -1630,6 +1630,11 @@ SessionPrivate::HandleResult SessionPrivate::handleOneReceived(
|
|||
_sessionSalt = data.vnew_server_salt().v;
|
||||
correctUnixtimeWithBadLocal(info.serverTime);
|
||||
|
||||
if (_bindMsgId) {
|
||||
LOG(("Message Info: bad_server_salt received while binding temp key, restarting."));
|
||||
return HandleResult::RestartConnection;
|
||||
}
|
||||
|
||||
if (setState(ConnectedState, ConnectingState)) {
|
||||
resendAll();
|
||||
}
|
||||
|
|
|
@ -224,9 +224,20 @@ rpl::producer<> DefaultOverlayWidgetHelper::controlsActivations() {
|
|||
}
|
||||
|
||||
rpl::producer<bool> DefaultOverlayWidgetHelper::controlsSideRightValue() {
|
||||
return Ui::Platform::TitleControlsLayoutValue() | rpl::map([=] {
|
||||
return _controls->controls.geometry().center().x()
|
||||
> _controls->wrap.geometry().center().x();
|
||||
using namespace Ui::Platform;
|
||||
|
||||
return TitleControlsLayoutValue(
|
||||
) | rpl::map([=](const TitleControls::Layout &layout) {
|
||||
// See TitleControls::updateControlsPosition.
|
||||
if (ranges::contains(layout.left, TitleControl::Close)) {
|
||||
return false;
|
||||
} else if (ranges::contains(layout.right, TitleControl::Close)) {
|
||||
return true;
|
||||
} else if (layout.left.size() > layout.right.size()) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}) | rpl::distinct_until_changed();
|
||||
}
|
||||
|
||||
|
|
|
@ -540,11 +540,53 @@ powerSavingButtonNoIcon: SettingsButton(powerSavingButton) {
|
|||
powerSavingSubtitlePadding: margins(0px, 4px, 0px, -2px);
|
||||
|
||||
filterInviteBox: Box(defaultBox) {
|
||||
buttonPadding: margins(12px, 12px, 12px, 12px);
|
||||
buttonHeight: 44px;
|
||||
buttonPadding: margins(10px, 9px, 10px, 9px);
|
||||
buttonHeight: 42px;
|
||||
button: RoundButton(defaultActiveButton) {
|
||||
height: 44px;
|
||||
height: 42px;
|
||||
textTop: 12px;
|
||||
font: font(13px semibold);
|
||||
}
|
||||
}
|
||||
filterInviteButtonStyle: TextStyle(defaultTextStyle) {
|
||||
font: font(13px semibold);
|
||||
linkFont: font(13px underline);
|
||||
linkFontOver: font(13px underline);
|
||||
}
|
||||
filterInviteButtonBadgeStyle: TextStyle(defaultTextStyle) {
|
||||
font: font(12px semibold);
|
||||
linkFont: font(12px underline);
|
||||
linkFontOver: font(12px underline);
|
||||
}
|
||||
filterInviteButtonBadgePadding: margins(5px, 0px, 5px, 2px);
|
||||
filterInviteButtonBadgeSkip: 5px;
|
||||
filterLinkTitlePadding: margins(0px, 15px, 0px, 17px);
|
||||
filterLinkAboutTextStyle: TextStyle(defaultTextStyle) {
|
||||
font: font(12px);
|
||||
linkFont: font(12px underline);
|
||||
linkFontOver: font(12px underline);
|
||||
lineHeight: 17px;
|
||||
}
|
||||
filterLinkAbout: FlatLabel(defaultFlatLabel) {
|
||||
style: filterLinkAboutTextStyle;
|
||||
align: align(top);
|
||||
minWidth: 190px;
|
||||
}
|
||||
filterLinkAboutTop: 170px;
|
||||
filterLinkAboutBottom: 15px;
|
||||
filterLinkPreview: 96px;
|
||||
filterLinkPreviewRadius: 13px;
|
||||
filterLinkPreviewTop: 30px;
|
||||
filterLinkPreviewColumn: 65px;
|
||||
filterLinkPreviewAllBottom: 18px;
|
||||
filterLinkPreviewAllTop: 17px;
|
||||
filterLinkPreviewMyBottom: 74px;
|
||||
filterLinkPreviewMyTop: 73px;
|
||||
filterLinkPreviewChatSize: 36px;
|
||||
filterLinkPreviewChatSkip: 10px;
|
||||
filterLinkPreviewBadgeLeft: 40px;
|
||||
filterLinkPreviewBadgeTop: 38px;
|
||||
filterLinkSubsectionTitlePadding: margins(0px, 5px, 0px, -4px);
|
||||
filterLinkChatsList: PeerList(peerListBox) {
|
||||
padding: margins(0px, 0px, 0px, membersMarginBottom);
|
||||
}
|
||||
|
|
446
Telegram/SourceFiles/ui/controls/filter_link_header.cpp
Normal file
446
Telegram/SourceFiles/ui/controls/filter_link_header.cpp
Normal file
|
@ -0,0 +1,446 @@
|
|||
/*
|
||||
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 "ui/controls/filter_link_header.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "styles/style_filter_icons.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_settings.h"
|
||||
#include "styles/style_window.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kBodyAnimationPart = 0.90;
|
||||
constexpr auto kTitleAdditionalScale = 0.05;
|
||||
|
||||
class Widget final : public RpWidget {
|
||||
public:
|
||||
Widget(
|
||||
not_null<QWidget*> parent,
|
||||
FilterLinkHeaderDescriptor &&descriptor);
|
||||
|
||||
void setTitlePosition(int x, int y);
|
||||
void updateDimensions(int newWidth);
|
||||
|
||||
[[nodiscard]] rpl::producer<not_null<QWheelEvent*>> wheelEvents() const;
|
||||
|
||||
private:
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void wheelEvent(QWheelEvent *e) override;
|
||||
|
||||
[[nodiscard]] QRectF previewRect(
|
||||
float64 topProgress,
|
||||
float64 sizeProgress) const;
|
||||
void refreshTitleText();
|
||||
|
||||
const not_null<FlatLabel*> _about;
|
||||
QMargins _aboutPadding;
|
||||
|
||||
struct {
|
||||
float64 top = 0.;
|
||||
float64 body = 0.;
|
||||
float64 title = 0.;
|
||||
float64 scaleTitle = 0.;
|
||||
} _progress;
|
||||
|
||||
rpl::variable<int> _badge;
|
||||
QImage _preview;
|
||||
QRectF _previewRect;
|
||||
|
||||
QString _titleText;
|
||||
style::font _titleFont;
|
||||
QMargins _titlePadding;
|
||||
QPoint _titlePosition;
|
||||
QPainterPath _titlePath;
|
||||
|
||||
QString _folderTitle;
|
||||
not_null<const style::icon*> _folderIcon;
|
||||
|
||||
int _maxHeight = 0;
|
||||
|
||||
rpl::event_stream<not_null<QWheelEvent*>> _wheelEvents;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] QImage GeneratePreview(
|
||||
const QString &title,
|
||||
not_null<const style::icon*> icon,
|
||||
int badge) {
|
||||
const auto size = st::filterLinkPreview;
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto radius = st::filterLinkPreviewRadius;
|
||||
const auto full = QSize(size, size) * ratio;
|
||||
auto result = QImage(full, QImage::Format_ARGB32_Premultiplied);
|
||||
result.setDevicePixelRatio(ratio);
|
||||
result.fill(st::windowBg->c);
|
||||
|
||||
auto p = QPainter(&result);
|
||||
|
||||
const auto column = st::filterLinkPreviewColumn;
|
||||
p.fillRect(0, 0, column, size, st::sideBarBg);
|
||||
p.fillRect(column, 0, size - column, size, st::emojiPanCategories);
|
||||
|
||||
const auto &st = st::windowFiltersButton;
|
||||
const auto skip = st.style.font->spacew;
|
||||
const auto available = column - 2 * skip;
|
||||
const auto iconWidth = st::foldersAll.width();
|
||||
const auto iconHeight = st::foldersAll.height();
|
||||
const auto iconLeft = (column - iconWidth) / 2;
|
||||
const auto allIconTop = st::filterLinkPreviewAllBottom - iconHeight;
|
||||
st::foldersAll.paint(p, iconLeft, allIconTop, size);
|
||||
const auto myIconTop = st::filterLinkPreviewMyBottom - iconHeight;
|
||||
icon->paint(p, iconLeft, myIconTop, size);
|
||||
|
||||
const auto paintName = [&](const QString &text, int top) {
|
||||
const auto &font = st.style.font;
|
||||
p.drawText(
|
||||
QRect(0, top, column, font->height),
|
||||
font->elided(text, available),
|
||||
style::al_top);
|
||||
};
|
||||
p.setFont(st.style.font);
|
||||
p.setPen(st.textFg);
|
||||
paintName(tr::lng_filters_all(tr::now), st::filterLinkPreviewAllTop);
|
||||
p.setPen(st.textFgActive);
|
||||
paintName(title, st::filterLinkPreviewMyTop);
|
||||
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
|
||||
const auto chatSize = st::filterLinkPreviewChatSize;
|
||||
const auto chatLeft = size + st::lineWidth - (chatSize / 2);
|
||||
const auto paintChat = [&](int top, const style::color &bg) {
|
||||
p.setBrush(bg);
|
||||
p.drawEllipse(chatLeft, top, chatSize, chatSize);
|
||||
};
|
||||
const auto chatSkip = st::filterLinkPreviewChatSkip;
|
||||
const auto chat1Top = (size - 2 * chatSize - chatSkip) / 2;
|
||||
const auto chat2Top = size - chat1Top - chatSize;
|
||||
p.setPen(Qt::NoPen);
|
||||
paintChat(chat1Top, st::historyPeer4UserpicBg);
|
||||
paintChat(chat2Top, st::historyPeer8UserpicBg);
|
||||
|
||||
if (badge > 0) {
|
||||
const auto font = st.badgeStyle.font;
|
||||
const auto badgeHeight = st.badgeHeight;
|
||||
const auto countBadgeWidth = [&](const QString &text) {
|
||||
return std::max(
|
||||
font->width(text) + 2 * st.badgeSkip,
|
||||
badgeHeight);
|
||||
};
|
||||
const auto defaultBadgeWidth = countBadgeWidth(u"+3"_q);
|
||||
const auto badgeText = '+' + QString::number(badge);
|
||||
const auto badgeWidth = countBadgeWidth(badgeText);
|
||||
const auto defaultBadgeLeft = st::filterLinkPreviewBadgeLeft;
|
||||
const auto badgeLeft = defaultBadgeLeft
|
||||
+ (defaultBadgeWidth - badgeWidth) / 2;
|
||||
const auto badgeTop = st::filterLinkPreviewBadgeTop;
|
||||
|
||||
const auto add = st::lineWidth;
|
||||
auto pen = st.textBg->p;
|
||||
pen.setWidthF(add * 2.);
|
||||
p.setPen(pen);
|
||||
p.setBrush(st.badgeBg);
|
||||
const auto radius = (badgeHeight / 2) + add;
|
||||
const auto rect = QRect(badgeLeft, badgeTop, badgeWidth, badgeHeight)
|
||||
+ QMargins(add, add, add, add);
|
||||
p.drawRoundedRect(rect, radius, radius);
|
||||
|
||||
p.setPen(st.badgeFg);
|
||||
p.setFont(st.badgeStyle.font);
|
||||
p.drawText(rect, badgeText, style::al_center);
|
||||
}
|
||||
|
||||
auto pen = st::shadowFg->p;
|
||||
pen.setWidthF(st::lineWidth * 2.);
|
||||
p.setPen(pen);
|
||||
p.setBrush(Qt::NoBrush);
|
||||
p.drawRoundedRect(0, 0, size, size, radius, radius);
|
||||
p.end();
|
||||
|
||||
return Images::Round(std::move(result), Images::CornersMask(radius));
|
||||
}
|
||||
|
||||
Widget::Widget(
|
||||
not_null<QWidget*> parent,
|
||||
FilterLinkHeaderDescriptor &&descriptor)
|
||||
: RpWidget(parent)
|
||||
, _about(CreateChild<Ui::FlatLabel>(
|
||||
this,
|
||||
rpl::single(descriptor.about.value()),
|
||||
st::filterLinkAbout))
|
||||
, _aboutPadding(st::boxRowPadding)
|
||||
, _badge(std::move(descriptor.badge))
|
||||
, _titleText(descriptor.title)
|
||||
, _titleFont(st::boxTitle.style.font)
|
||||
, _titlePadding(st::filterLinkTitlePadding)
|
||||
, _folderTitle(descriptor.folderTitle)
|
||||
, _folderIcon(descriptor.folderIcon) {
|
||||
setMinimumHeight(st::boxTitleHeight);
|
||||
refreshTitleText();
|
||||
setTitlePosition(st::boxTitlePosition.x(), st::boxTitlePosition.y());
|
||||
|
||||
style::PaletteChanged(
|
||||
) | rpl::start_with_next([=] {
|
||||
_preview = QImage();
|
||||
}, lifetime());
|
||||
|
||||
_badge.changes() | rpl::start_with_next([=] {
|
||||
_preview = QImage();
|
||||
update();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void Widget::refreshTitleText() {
|
||||
_titlePath = QPainterPath();
|
||||
_titlePath.addText(0, _titleFont->ascent, _titleFont, _titleText);
|
||||
update();
|
||||
}
|
||||
|
||||
void Widget::setTitlePosition(int x, int y) {
|
||||
_titlePosition = { x, y };
|
||||
}
|
||||
|
||||
rpl::producer<not_null<QWheelEvent*>> Widget::wheelEvents() const {
|
||||
return _wheelEvents.events();
|
||||
}
|
||||
|
||||
void Widget::resizeEvent(QResizeEvent *e) {
|
||||
const auto &padding = _aboutPadding;
|
||||
const auto availableWidth = width() - padding.left() - padding.right();
|
||||
if (availableWidth <= 0) {
|
||||
return;
|
||||
}
|
||||
_about->resizeToWidth(availableWidth);
|
||||
|
||||
const auto minHeight = minimumHeight();
|
||||
const auto maxHeight = st::filterLinkAboutTop
|
||||
+ _about->height()
|
||||
+ st::filterLinkAboutBottom;
|
||||
if (maxHeight <= minHeight) {
|
||||
return;
|
||||
} else if (_maxHeight != maxHeight) {
|
||||
_maxHeight = maxHeight;
|
||||
setMaximumHeight(maxHeight);
|
||||
}
|
||||
|
||||
const auto progress = (height() - minHeight)
|
||||
/ float64(_maxHeight - minHeight);
|
||||
_progress.top = 1. -
|
||||
std::clamp(
|
||||
(1. - progress) / kBodyAnimationPart,
|
||||
0.,
|
||||
1.);
|
||||
_progress.body = _progress.top;
|
||||
_progress.title = 1. - progress;
|
||||
_progress.scaleTitle = 1. + kTitleAdditionalScale * progress;
|
||||
|
||||
_previewRect = previewRect(_progress.top, _progress.body);
|
||||
|
||||
const auto titleTop = _previewRect.top()
|
||||
+ _previewRect.height()
|
||||
+ _titlePadding.top();
|
||||
const auto titlePathRect = _titlePath.boundingRect();
|
||||
const auto aboutTop = titleTop
|
||||
+ titlePathRect.height()
|
||||
+ _titlePadding.bottom();
|
||||
_about->moveToLeft(_aboutPadding.left(), aboutTop);
|
||||
_about->setOpacity(_progress.body);
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
QRectF Widget::previewRect(
|
||||
float64 topProgress,
|
||||
float64 sizeProgress) const {
|
||||
const auto size = st::filterLinkPreview;
|
||||
return QRectF(
|
||||
(width() - size) / 2.,
|
||||
st::filterLinkPreviewTop * topProgress,
|
||||
size,
|
||||
size);
|
||||
};
|
||||
|
||||
void Widget::paintEvent(QPaintEvent *e) {
|
||||
auto p = QPainter(this);
|
||||
|
||||
p.setOpacity(_progress.body);
|
||||
p.translate(_previewRect.center());
|
||||
p.scale(_progress.body, _progress.body);
|
||||
p.translate(-_previewRect.center());
|
||||
if (_progress.top) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
if (_preview.isNull()) {
|
||||
_preview = GeneratePreview(
|
||||
_folderTitle,
|
||||
_folderIcon,
|
||||
_badge.current());
|
||||
}
|
||||
p.drawImage(_previewRect, _preview);
|
||||
}
|
||||
p.resetTransform();
|
||||
|
||||
const auto titlePathRect = _titlePath.boundingRect();
|
||||
|
||||
// Title.
|
||||
PainterHighQualityEnabler hq(p);
|
||||
p.setOpacity(1.);
|
||||
p.setFont(_titleFont);
|
||||
p.setPen(st::boxTitleFg);
|
||||
const auto fullPreviewRect = previewRect(1., 1.);
|
||||
const auto fullTitleTop = fullPreviewRect.top()
|
||||
+ fullPreviewRect.height()
|
||||
+ _titlePadding.top();
|
||||
p.translate(
|
||||
anim::interpolate(
|
||||
(width() - titlePathRect.width()) / 2,
|
||||
_titlePosition.x(),
|
||||
_progress.title),
|
||||
anim::interpolate(fullTitleTop, _titlePosition.y(), _progress.title));
|
||||
|
||||
p.translate(titlePathRect.center());
|
||||
p.scale(_progress.scaleTitle, _progress.scaleTitle);
|
||||
p.translate(-titlePathRect.center());
|
||||
p.fillPath(_titlePath, st::boxTitleFg);
|
||||
}
|
||||
|
||||
void Widget::wheelEvent(QWheelEvent *e) {
|
||||
_wheelEvents.fire(e);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
[[nodiscard]] FilterLinkHeader MakeFilterLinkHeader(
|
||||
not_null<QWidget*> parent,
|
||||
FilterLinkHeaderDescriptor &&descriptor) {
|
||||
const auto result = CreateChild<Widget>(
|
||||
parent.get(),
|
||||
std::move(descriptor));
|
||||
return { .widget = result, .wheelEvents = result->wheelEvents() };
|
||||
}
|
||||
|
||||
object_ptr<RoundButton> FilterLinkProcessButton(
|
||||
not_null<QWidget*> parent,
|
||||
FilterLinkHeaderType type,
|
||||
const QString &title,
|
||||
rpl::producer<int> badge) {
|
||||
const auto st = &st::filterInviteBox.button;
|
||||
const auto badgeSt = &st::filterInviteButtonBadgeStyle;
|
||||
auto result = object_ptr<RoundButton>(parent, rpl::single(u""_q), *st);
|
||||
|
||||
struct Data {
|
||||
QString text;
|
||||
QString badge;
|
||||
};
|
||||
auto data = std::move(
|
||||
badge
|
||||
) | rpl::map([=](int count) {
|
||||
const auto badge = count ? QString::number(count) : QString();
|
||||
const auto with = [&](QString badge) {
|
||||
return rpl::map([=](QString text) {
|
||||
return Data{ text, badge };
|
||||
});
|
||||
};
|
||||
switch (type) {
|
||||
case FilterLinkHeaderType::AddingFilter:
|
||||
return badge.isEmpty()
|
||||
? tr::lng_filters_by_link_add_no() | with(QString())
|
||||
: tr::lng_filters_by_link_add_button(
|
||||
lt_folder,
|
||||
rpl::single(title)
|
||||
) | with(badge);
|
||||
case FilterLinkHeaderType::AddingChats:
|
||||
return badge.isEmpty()
|
||||
? tr::lng_filters_by_link_join_no() | with(QString())
|
||||
: tr::lng_filters_by_link_join_button() | with(badge);
|
||||
case FilterLinkHeaderType::AllAdded:
|
||||
return tr::lng_box_ok() | with(QString());
|
||||
case FilterLinkHeaderType::Removing:
|
||||
return badge.isEmpty()
|
||||
? tr::lng_filters_by_link_remove_button() | with(QString())
|
||||
: tr::lng_filters_by_link_quit_button() | with(badge);
|
||||
}
|
||||
Unexpected("Type in FilterLinkProcessButton.");
|
||||
}) | rpl::flatten_latest();
|
||||
|
||||
struct Label : RpWidget {
|
||||
using RpWidget::RpWidget;
|
||||
|
||||
Text::String text;
|
||||
Text::String badge;
|
||||
};
|
||||
const auto label = result->lifetime().make_state<Label>(result.data());
|
||||
label->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
result->sizeValue() | rpl::start_with_next([=](QSize size) {
|
||||
const auto xskip = st->font->spacew;
|
||||
const auto yskip = xskip / 2;
|
||||
label->setGeometry(QRect(QPoint(), size).marginsRemoved(
|
||||
{ xskip, yskip, xskip, yskip }));
|
||||
}, label->lifetime());
|
||||
label->paintRequest(
|
||||
) | rpl::start_with_next([=] {
|
||||
auto p = Painter(label);
|
||||
const auto width = label->width();
|
||||
const auto hasBadge = !label->badge.isEmpty();
|
||||
const auto &badgePadding = st::filterInviteButtonBadgePadding;
|
||||
const auto badgeInnerWidth = label->badge.maxWidth();
|
||||
const auto badgeInnerHeight = badgeSt->font->height;
|
||||
const auto badgeSize = QRect(
|
||||
0,
|
||||
0,
|
||||
badgeInnerWidth,
|
||||
badgeInnerHeight
|
||||
).marginsAdded(badgePadding).size();
|
||||
const auto skip = st::filterInviteButtonBadgeSkip;
|
||||
const auto badgeWithSkip = hasBadge ? (skip + badgeSize.width()) : 0;
|
||||
const auto full = label->text.maxWidth() + badgeWithSkip;
|
||||
const auto use = std::min(full, width);
|
||||
const auto left = (width - use) / 2;
|
||||
const auto top = st->textTop - label->y();
|
||||
const auto available = use - badgeWithSkip;
|
||||
|
||||
p.setPen(st->textFg);
|
||||
label->text.drawLeftElided(p, left, top, available + skip, width);
|
||||
if (hasBadge) {
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st->textFg);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto radius = badgeSize.height() / 2;
|
||||
const auto badgePosition = QPoint(
|
||||
left + available + skip,
|
||||
top - badgePadding.top());
|
||||
p.drawRoundedRect(
|
||||
QRect(badgePosition, badgeSize),
|
||||
radius,
|
||||
radius);
|
||||
p.setPen(st->textBg);
|
||||
label->badge.drawLeftElided(
|
||||
p,
|
||||
badgePosition.x() + badgePadding.left(),
|
||||
badgePosition.y() + badgePadding.top(),
|
||||
badgeInnerWidth + skip,
|
||||
width);
|
||||
}
|
||||
}, label->lifetime());
|
||||
|
||||
std::move(data) | rpl::start_with_next([=](Data data) {
|
||||
label->text.setText(st::filterInviteButtonStyle, data.text);
|
||||
label->badge.setText(st::filterInviteButtonBadgeStyle, data.badge);
|
||||
label->update();
|
||||
}, label->lifetime());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
49
Telegram/SourceFiles/ui/controls/filter_link_header.h
Normal file
49
Telegram/SourceFiles/ui/controls/filter_link_header.h
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
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 "base/required.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class RpWidget;
|
||||
class RoundButton;
|
||||
|
||||
enum class FilterLinkHeaderType {
|
||||
AddingFilter,
|
||||
AddingChats,
|
||||
AllAdded,
|
||||
Removing,
|
||||
};
|
||||
|
||||
struct FilterLinkHeaderDescriptor {
|
||||
base::required<FilterLinkHeaderType> type;
|
||||
base::required<QString> title;
|
||||
base::required<TextWithEntities> about;
|
||||
base::required<QString> folderTitle;
|
||||
not_null<const style::icon*> folderIcon;
|
||||
rpl::producer<int> badge;
|
||||
};
|
||||
|
||||
struct FilterLinkHeader {
|
||||
not_null<RpWidget*> widget;
|
||||
rpl::producer<not_null<QWheelEvent*>> wheelEvents;
|
||||
};
|
||||
|
||||
[[nodiscard]] FilterLinkHeader MakeFilterLinkHeader(
|
||||
not_null<QWidget*> parent,
|
||||
FilterLinkHeaderDescriptor &&descriptor);
|
||||
|
||||
[[nodiscard]] object_ptr<RoundButton> FilterLinkProcessButton(
|
||||
not_null<QWidget*> parent,
|
||||
FilterLinkHeaderType type,
|
||||
const QString &title,
|
||||
rpl::producer<int> badge);
|
||||
|
||||
} // namespace Ui
|
|
@ -236,6 +236,8 @@ PRIVATE
|
|||
ui/controls/download_bar.h
|
||||
ui/controls/emoji_button.cpp
|
||||
ui/controls/emoji_button.h
|
||||
ui/controls/filter_link_header.cpp
|
||||
ui/controls/filter_link_header.h
|
||||
ui/controls/jump_down_button.cpp
|
||||
ui/controls/jump_down_button.h
|
||||
ui/controls/invite_link_buttons.cpp
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit f5fb567052d9ce47c3c694ef81936e28a1c7315e
|
||||
Subproject commit aa5cc61b6b65ec1fb8f1c1be8b2c9c559aadd958
|
Loading…
Add table
Reference in a new issue