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
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
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
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
This commit is contained in:
83
Telegram/SourceFiles/dialogs/ui/chat_search_empty.cpp
Normal file
83
Telegram/SourceFiles/dialogs/ui/chat_search_empty.cpp
Normal file
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
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 "dialogs/ui/chat_search_empty.h"
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
#include "lottie/lottie_icon.h"
|
||||
#include "settings/settings_common.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
SearchEmpty::SearchEmpty(
|
||||
QWidget *parent,
|
||||
Icon icon,
|
||||
rpl::producer<TextWithEntities> text)
|
||||
: RpWidget(parent) {
|
||||
setup(icon, std::move(text));
|
||||
}
|
||||
|
||||
void SearchEmpty::setMinimalHeight(int minimalHeight) {
|
||||
const auto minimal = st::recentPeersEmptyHeightMin;
|
||||
resize(width(), std::max(minimalHeight, minimal));
|
||||
}
|
||||
|
||||
void SearchEmpty::setup(Icon icon, rpl::producer<TextWithEntities> text) {
|
||||
const auto label = Ui::CreateChild<Ui::FlatLabel>(
|
||||
this,
|
||||
std::move(text),
|
||||
st::defaultPeerListAbout);
|
||||
const auto size = st::recentPeersEmptySize;
|
||||
const auto animation = [&] {
|
||||
switch (icon) {
|
||||
case Icon::Search: return u"search"_q;
|
||||
case Icon::NoResults: return u"noresults"_q;
|
||||
}
|
||||
Unexpected("Icon in SearchEmpty::setup.");
|
||||
}();
|
||||
const auto [widget, animate] = Settings::CreateLottieIcon(
|
||||
this,
|
||||
{
|
||||
.name = animation,
|
||||
.sizeOverride = { size, size },
|
||||
},
|
||||
st::recentPeersEmptyMargin);
|
||||
const auto animated = widget.data();
|
||||
|
||||
sizeValue() | rpl::on_next([=](QSize size) {
|
||||
const auto padding = st::recentPeersEmptyMargin;
|
||||
const auto paddings = padding.left() + padding.right();
|
||||
label->resizeToWidth(size.width() - paddings);
|
||||
const auto x = (size.width() - animated->width()) / 2;
|
||||
const auto y = (size.height() - animated->height()) / 3;
|
||||
const auto top = y + animated->height() + st::recentPeersEmptySkip;
|
||||
const auto sub = std::max(top + label->height() - size.height(), 0);
|
||||
animated->move(x, y - sub);
|
||||
label->move((size.width() - label->width()) / 2, top - sub);
|
||||
}, lifetime());
|
||||
|
||||
label->setClickHandlerFilter([=](
|
||||
const ClickHandlerPtr &handler,
|
||||
Qt::MouseButton) {
|
||||
_handlerActivated.fire_copy(handler);
|
||||
return false;
|
||||
});
|
||||
|
||||
_animate = [animate] {
|
||||
animate(anim::repeat::once);
|
||||
};
|
||||
}
|
||||
|
||||
void SearchEmpty::animate() {
|
||||
if (const auto onstack = _animate) {
|
||||
onstack();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
44
Telegram/SourceFiles/dialogs/ui/chat_search_empty.h
Normal file
44
Telegram/SourceFiles/dialogs/ui/chat_search_empty.h
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
enum class SearchEmptyIcon {
|
||||
Search,
|
||||
NoResults,
|
||||
};
|
||||
|
||||
class SearchEmpty final : public Ui::RpWidget {
|
||||
public:
|
||||
using Icon = SearchEmptyIcon;
|
||||
|
||||
SearchEmpty(
|
||||
QWidget *parent,
|
||||
Icon icon,
|
||||
rpl::producer<TextWithEntities> text);
|
||||
|
||||
void setMinimalHeight(int minimalHeight);
|
||||
|
||||
void animate();
|
||||
|
||||
[[nodiscard]] rpl::producer<ClickHandlerPtr> handlerActivated() const {
|
||||
return _handlerActivated.events();
|
||||
}
|
||||
|
||||
private:
|
||||
void setup(Icon icon, rpl::producer<TextWithEntities> text);
|
||||
|
||||
Fn<void()> _animate;
|
||||
rpl::event_stream<ClickHandlerPtr> _handlerActivated;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Dialogs
|
||||
474
Telegram/SourceFiles/dialogs/ui/chat_search_in.cpp
Normal file
474
Telegram/SourceFiles/dialogs/ui/chat_search_in.cpp
Normal file
@@ -0,0 +1,474 @@
|
||||
/*
|
||||
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 "dialogs/ui/chat_search_in.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/widgets/shadow.h"
|
||||
#include "ui/widgets/menu/menu_item_base.h"
|
||||
#include "ui/dynamic_image.h"
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
#include "styles/style_window.h"
|
||||
|
||||
namespace Dialogs {
|
||||
namespace {
|
||||
|
||||
class Action final : public Ui::Menu::ItemBase {
|
||||
public:
|
||||
Action(
|
||||
not_null<Ui::PopupMenu*> parentMenu,
|
||||
std::shared_ptr<Ui::DynamicImage> icon,
|
||||
const QString &label,
|
||||
bool chosen);
|
||||
~Action();
|
||||
|
||||
bool isEnabled() const override;
|
||||
not_null<QAction*> action() const override;
|
||||
|
||||
void handleKeyPress(not_null<QKeyEvent*> e) override;
|
||||
|
||||
protected:
|
||||
QPoint prepareRippleStartPosition() const override;
|
||||
QImage prepareRippleMask() const override;
|
||||
|
||||
int contentHeight() const override;
|
||||
|
||||
private:
|
||||
void paint(Painter &p);
|
||||
|
||||
void resolveMinWidth();
|
||||
void refreshDimensions();
|
||||
|
||||
const not_null<Ui::PopupMenu*> _parentMenu;
|
||||
const not_null<QAction*> _dummyAction;
|
||||
const style::Menu &_st;
|
||||
const int _height = 0;
|
||||
|
||||
std::shared_ptr<Ui::DynamicImage> _icon;
|
||||
Ui::Text::String _text;
|
||||
bool _checked = false;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] QString TabLabel(
|
||||
ChatSearchTab tab,
|
||||
ChatSearchPeerTabType type = {}) {
|
||||
switch (tab) {
|
||||
case ChatSearchTab::MyMessages:
|
||||
return tr::lng_search_tab_my_messages(tr::now);
|
||||
case ChatSearchTab::ThisTopic:
|
||||
return tr::lng_search_tab_this_topic(tr::now);
|
||||
case ChatSearchTab::ThisPeer:
|
||||
switch (type) {
|
||||
case ChatSearchPeerTabType::Chat:
|
||||
return tr::lng_search_tab_this_chat(tr::now);
|
||||
case ChatSearchPeerTabType::Channel:
|
||||
return tr::lng_search_tab_this_channel(tr::now);
|
||||
case ChatSearchPeerTabType::Group:
|
||||
return tr::lng_search_tab_this_group(tr::now);
|
||||
}
|
||||
Unexpected("Type in Dialogs::TabLabel.");
|
||||
case ChatSearchTab::PublicPosts:
|
||||
return tr::lng_search_tab_public_posts(tr::now);
|
||||
}
|
||||
Unexpected("Tab in Dialogs::TabLabel.");
|
||||
}
|
||||
|
||||
Action::Action(
|
||||
not_null<Ui::PopupMenu*> parentMenu,
|
||||
std::shared_ptr<Ui::DynamicImage> icon,
|
||||
const QString &label,
|
||||
bool chosen)
|
||||
: ItemBase(parentMenu->menu(), parentMenu->menu()->st())
|
||||
, _parentMenu(parentMenu)
|
||||
, _dummyAction(CreateChild<QAction>(parentMenu->menu().get()))
|
||||
, _st(parentMenu->menu()->st())
|
||||
, _height(st::dialogsSearchInHeight)
|
||||
, _icon(std::move(icon))
|
||||
, _checked(chosen) {
|
||||
const auto parent = parentMenu->menu();
|
||||
|
||||
_text.setText(st::semiboldTextStyle, label);
|
||||
_icon->subscribeToUpdates([=] { update(); });
|
||||
|
||||
initResizeHook(parent->sizeValue());
|
||||
resolveMinWidth();
|
||||
|
||||
paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
Painter p(this);
|
||||
paint(p);
|
||||
}, lifetime());
|
||||
|
||||
enableMouseSelecting();
|
||||
}
|
||||
|
||||
Action::~Action() {
|
||||
_icon->subscribeToUpdates(nullptr);
|
||||
}
|
||||
|
||||
void Action::resolveMinWidth() {
|
||||
const auto maxWidth = st::dialogsSearchInPhotoPadding
|
||||
+ st::dialogsSearchInPhotoSize
|
||||
+ st::dialogsSearchInSkip
|
||||
+ _text.maxWidth()
|
||||
+ st::dialogsSearchInCheckSkip
|
||||
+ st::dialogsSearchInCheck.width()
|
||||
+ st::dialogsSearchInCheckSkip;
|
||||
setMinWidth(maxWidth);
|
||||
}
|
||||
|
||||
void Action::paint(Painter &p) {
|
||||
const auto enabled = isEnabled();
|
||||
const auto selected = isSelected();
|
||||
if (selected && _st.itemBgOver->c.alpha() < 255) {
|
||||
p.fillRect(0, 0, width(), _height, _st.itemBg);
|
||||
}
|
||||
const auto &bg = selected ? _st.itemBgOver : _st.itemBg;
|
||||
p.fillRect(0, 0, width(), _height, bg);
|
||||
if (enabled) {
|
||||
paintRipple(p, 0, 0);
|
||||
}
|
||||
|
||||
auto x = st::dialogsSearchInPhotoPadding;
|
||||
const auto photos = st::dialogsSearchInPhotoSize;
|
||||
const auto photoy = (height() - photos) / 2;
|
||||
p.drawImage(QRect{ x, photoy, photos, photos }, _icon->image(photos));
|
||||
x += photos + st::dialogsSearchInSkip;
|
||||
const auto available = width()
|
||||
- x
|
||||
- st::dialogsSearchInCheckSkip
|
||||
- st::dialogsSearchInCheck.width()
|
||||
- st::dialogsSearchInCheckSkip;
|
||||
|
||||
p.setPen(!enabled
|
||||
? _st.itemFgDisabled
|
||||
: selected
|
||||
? _st.itemFgOver
|
||||
: _st.itemFg);
|
||||
_text.drawLeftElided(
|
||||
p,
|
||||
x,
|
||||
st::dialogsSearchInNameTop,
|
||||
available,
|
||||
width());
|
||||
x += available;
|
||||
if (_checked) {
|
||||
x += st::dialogsSearchInCheckSkip;
|
||||
const auto &icon = st::dialogsSearchInCheck;
|
||||
const auto icony = (height() - icon.height()) / 2;
|
||||
icon.paint(p, x, icony, width());
|
||||
}
|
||||
}
|
||||
|
||||
bool Action::isEnabled() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
not_null<QAction*> Action::action() const {
|
||||
return _dummyAction;
|
||||
}
|
||||
|
||||
QPoint Action::prepareRippleStartPosition() const {
|
||||
return mapFromGlobal(QCursor::pos());
|
||||
}
|
||||
|
||||
QImage Action::prepareRippleMask() const {
|
||||
return Ui::RippleAnimation::RectMask(size());
|
||||
}
|
||||
|
||||
int Action::contentHeight() const {
|
||||
return _height;
|
||||
}
|
||||
|
||||
void Action::handleKeyPress(not_null<QKeyEvent*> e) {
|
||||
if (!isSelected()) {
|
||||
return;
|
||||
}
|
||||
const auto key = e->key();
|
||||
if (key == Qt::Key_Enter || key == Qt::Key_Return) {
|
||||
setClicked(Ui::Menu::TriggeredSource::Keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
FixedHashtagSearchQuery FixHashtagSearchQuery(
|
||||
const QString &query,
|
||||
int cursorPosition,
|
||||
HashOrCashtag tag) {
|
||||
const auto trimmed = query.trimmed();
|
||||
const auto hash = int(trimmed.isEmpty()
|
||||
? query.size()
|
||||
: query.indexOf(trimmed));
|
||||
const auto start = std::min(cursorPosition, hash);
|
||||
const auto first = QChar(tag == HashOrCashtag::Cashtag ? '$' : '#');
|
||||
auto result = query.mid(0, start);
|
||||
for (const auto &ch : query.mid(start)) {
|
||||
if (ch.isSpace()) {
|
||||
if (cursorPosition > result.size()) {
|
||||
--cursorPosition;
|
||||
}
|
||||
continue;
|
||||
} else if (result.size() == start) {
|
||||
result += first;
|
||||
if (ch != first) {
|
||||
++cursorPosition;
|
||||
}
|
||||
}
|
||||
if (ch != first) {
|
||||
result += ch;
|
||||
}
|
||||
}
|
||||
if (result.size() == start) {
|
||||
result += first;
|
||||
++cursorPosition;
|
||||
}
|
||||
return { result, cursorPosition };
|
||||
}
|
||||
|
||||
HashOrCashtag IsHashOrCashtagSearchQuery(const QString &query) {
|
||||
const auto trimmed = query.trimmed();
|
||||
const auto first = trimmed.isEmpty() ? QChar() : trimmed[0];
|
||||
if (first == '#') {
|
||||
for (const auto &ch : trimmed) {
|
||||
if (ch.isSpace()) {
|
||||
return HashOrCashtag::None;
|
||||
}
|
||||
}
|
||||
return HashOrCashtag::Hashtag;
|
||||
} else if (first == '$') {
|
||||
for (auto it = trimmed.begin() + 1; it != trimmed.end(); ++it) {
|
||||
if ((*it) < 'A' || (*it) > 'Z') {
|
||||
return HashOrCashtag::None;
|
||||
}
|
||||
}
|
||||
return HashOrCashtag::Cashtag;
|
||||
}
|
||||
return HashOrCashtag::None;
|
||||
}
|
||||
|
||||
void ChatSearchIn::Section::update() {
|
||||
outer->update();
|
||||
}
|
||||
|
||||
ChatSearchIn::ChatSearchIn(QWidget *parent)
|
||||
: RpWidget(parent) {
|
||||
_in.clicks.events() | rpl::on_next([=] {
|
||||
showMenu();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
ChatSearchIn::~ChatSearchIn() = default;
|
||||
|
||||
void ChatSearchIn::apply(
|
||||
std::vector<PossibleTab> tabs,
|
||||
ChatSearchTab active,
|
||||
ChatSearchPeerTabType peerTabType,
|
||||
std::shared_ptr<Ui::DynamicImage> fromUserpic,
|
||||
QString fromName) {
|
||||
_tabs = std::move(tabs);
|
||||
_peerTabType = peerTabType;
|
||||
_active = active;
|
||||
const auto i = ranges::find(_tabs, active, &PossibleTab::tab);
|
||||
Assert(i != end(_tabs));
|
||||
Assert(i->icon != nullptr);
|
||||
updateSection(
|
||||
&_in,
|
||||
i->icon->clone(),
|
||||
tr::semibold(TabLabel(active, peerTabType)));
|
||||
|
||||
auto text = tr::lng_dlg_search_from(
|
||||
tr::now,
|
||||
lt_user,
|
||||
tr::semibold(fromName),
|
||||
tr::marked);
|
||||
updateSection(&_from, std::move(fromUserpic), std::move(text));
|
||||
|
||||
resizeToWidth(width());
|
||||
}
|
||||
|
||||
rpl::producer<> ChatSearchIn::cancelInRequests() const {
|
||||
return _in.cancelRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<> ChatSearchIn::cancelFromRequests() const {
|
||||
return _from.cancelRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<> ChatSearchIn::changeFromRequests() const {
|
||||
return _from.clicks.events();
|
||||
}
|
||||
|
||||
rpl::producer<ChatSearchTab> ChatSearchIn::tabChanges() const {
|
||||
return _active.changes();
|
||||
}
|
||||
|
||||
void ChatSearchIn::showMenu() {
|
||||
_menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
this,
|
||||
st::dialogsSearchInMenu);
|
||||
const auto active = _active.current();
|
||||
auto activeIndex = 0;
|
||||
for (const auto &tab : _tabs) {
|
||||
if (!tab.icon) {
|
||||
continue;
|
||||
}
|
||||
const auto value = tab.tab;
|
||||
if (value == active) {
|
||||
activeIndex = _menu->actions().size();
|
||||
}
|
||||
auto action = base::make_unique_q<Action>(
|
||||
_menu.get(),
|
||||
tab.icon,
|
||||
TabLabel(value, _peerTabType),
|
||||
(value == active));
|
||||
action->setClickedCallback([=] {
|
||||
_active = value;
|
||||
});
|
||||
_menu->addAction(std::move(action));
|
||||
}
|
||||
const auto count = int(_menu->actions().size());
|
||||
const auto bottomLeft = (activeIndex * 2 >= count);
|
||||
const auto single = st::dialogsSearchInHeight;
|
||||
const auto in = mapToGlobal(_in.outer->pos()
|
||||
+ QPoint(0, bottomLeft ? count * single : 0));
|
||||
_menu->setForcedOrigin(bottomLeft
|
||||
? Ui::PanelAnimation::Origin::BottomLeft
|
||||
: Ui::PanelAnimation::Origin::TopLeft);
|
||||
if (_menu->prepareGeometryFor(in)) {
|
||||
_menu->move(_menu->pos() - QPoint(_menu->inner().x(), activeIndex * single));
|
||||
_menu->popupPrepared();
|
||||
}
|
||||
}
|
||||
|
||||
void ChatSearchIn::paintEvent(QPaintEvent *e) {
|
||||
auto p = Painter(this);
|
||||
const auto top = QRect(0, 0, width(), st::searchedBarHeight);
|
||||
p.fillRect(top, st::searchedBarBg);
|
||||
p.fillRect(rect().translated(0, st::searchedBarHeight), st::dialogsBg);
|
||||
|
||||
p.setFont(st::searchedBarFont);
|
||||
p.setPen(st::searchedBarFg);
|
||||
p.drawTextLeft(
|
||||
st::searchedBarPosition.x(),
|
||||
st::searchedBarPosition.y(),
|
||||
width(),
|
||||
tr::lng_dlg_search_in(tr::now));
|
||||
}
|
||||
|
||||
int ChatSearchIn::resizeGetHeight(int newWidth) {
|
||||
auto result = st::searchedBarHeight;
|
||||
if (const auto raw = _in.outer.get()) {
|
||||
raw->resizeToWidth(newWidth);
|
||||
raw->move(0, result);
|
||||
result += raw->height();
|
||||
_in.shadow->setGeometry(0, result, newWidth, st::lineWidth);
|
||||
result += st::lineWidth;
|
||||
}
|
||||
if (const auto raw = _from.outer.get()) {
|
||||
raw->resizeToWidth(newWidth);
|
||||
raw->move(0, result);
|
||||
result += raw->height();
|
||||
_from.shadow->setGeometry(0, result, newWidth, st::lineWidth);
|
||||
result += st::lineWidth;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void ChatSearchIn::updateSection(
|
||||
not_null<Section*> section,
|
||||
std::shared_ptr<Ui::DynamicImage> image,
|
||||
TextWithEntities text) {
|
||||
if (section->subscribed) {
|
||||
section->image->subscribeToUpdates(nullptr);
|
||||
section->subscribed = false;
|
||||
}
|
||||
if (!image) {
|
||||
if (section->outer) {
|
||||
section->cancel = nullptr;
|
||||
section->shadow = nullptr;
|
||||
section->outer = nullptr;
|
||||
section->subscribed = false;
|
||||
}
|
||||
return;
|
||||
} else if (!section->outer) {
|
||||
auto button = std::make_unique<Ui::AbstractButton>(this);
|
||||
const auto raw = button.get();
|
||||
section->outer = std::move(button);
|
||||
|
||||
raw->resize(
|
||||
st::columnMinimalWidthLeft,
|
||||
st::dialogsSearchInHeight);
|
||||
|
||||
raw->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(raw);
|
||||
if (!section->subscribed) {
|
||||
section->subscribed = true;
|
||||
section->image->subscribeToUpdates([=] {
|
||||
raw->update();
|
||||
});
|
||||
}
|
||||
const auto outer = raw->width();
|
||||
const auto size = st::dialogsSearchInPhotoSize;
|
||||
const auto left = st::dialogsSearchInPhotoPadding;
|
||||
const auto top = (st::dialogsSearchInHeight - size) / 2;
|
||||
p.drawImage(
|
||||
QRect{ left, top, size, size },
|
||||
section->image->image(size));
|
||||
|
||||
const auto x = left + size + st::dialogsSearchInSkip;
|
||||
const auto available = outer
|
||||
- st::dialogsSearchInSkip
|
||||
- section->cancel->width()
|
||||
- 2 * st::dialogsSearchInDownSkip
|
||||
- st::dialogsSearchInDown.width()
|
||||
- x;
|
||||
const auto use = std::min(section->text.maxWidth(), available);
|
||||
const auto iconx = x + use + st::dialogsSearchInDownSkip;
|
||||
const auto icony = st::dialogsSearchInDownTop;
|
||||
st::dialogsSearchInDown.paint(p, iconx, icony, outer);
|
||||
p.setPen(st::windowBoldFg);
|
||||
section->text.draw(p, {
|
||||
.position = QPoint(x, st::dialogsSearchInNameTop),
|
||||
.outerWidth = outer,
|
||||
.availableWidth = available,
|
||||
.elisionLines = 1,
|
||||
});
|
||||
}, raw->lifetime());
|
||||
|
||||
section->shadow = std::make_unique<Ui::PlainShadow>(this);
|
||||
section->shadow->show();
|
||||
|
||||
const auto st = &st::dialogsCancelSearchInPeer;
|
||||
section->cancel = std::make_unique<Ui::IconButton>(raw, *st);
|
||||
section->cancel->show();
|
||||
raw->sizeValue() | rpl::on_next([=](QSize size) {
|
||||
const auto left = size.width() - section->cancel->width();
|
||||
const auto top = (size.height() - st->height) / 2;
|
||||
section->cancel->moveToLeft(left, top);
|
||||
}, section->cancel->lifetime());
|
||||
section->cancel->clicks() | rpl::to_empty | rpl::start_to_stream(
|
||||
section->cancelRequests,
|
||||
section->cancel->lifetime());
|
||||
|
||||
raw->clicks() | rpl::to_empty | rpl::start_to_stream(
|
||||
section->clicks,
|
||||
raw->lifetime());
|
||||
|
||||
raw->show();
|
||||
}
|
||||
section->image = std::move(image);
|
||||
section->text.setMarkedText(st::dialogsSearchFromStyle, std::move(text));
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
107
Telegram/SourceFiles/dialogs/ui/chat_search_in.h
Normal file
107
Telegram/SourceFiles/dialogs/ui/chat_search_in.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/unique_qptr.h"
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
namespace Ui {
|
||||
class PlainShadow;
|
||||
class DynamicImage;
|
||||
class IconButton;
|
||||
class PopupMenu;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
enum class ChatSearchTab : uchar {
|
||||
MyMessages,
|
||||
ThisTopic,
|
||||
ThisPeer,
|
||||
PublicPosts,
|
||||
};
|
||||
|
||||
enum class ChatSearchPeerTabType : uchar {
|
||||
Chat,
|
||||
Channel,
|
||||
Group,
|
||||
};
|
||||
|
||||
class ChatSearchIn final : public Ui::RpWidget {
|
||||
public:
|
||||
explicit ChatSearchIn(QWidget *parent);
|
||||
~ChatSearchIn();
|
||||
|
||||
struct PossibleTab {
|
||||
ChatSearchTab tab = {};
|
||||
std::shared_ptr<Ui::DynamicImage> icon;
|
||||
};
|
||||
void apply(
|
||||
std::vector<PossibleTab> tabs,
|
||||
ChatSearchTab active,
|
||||
ChatSearchPeerTabType peerTabType,
|
||||
std::shared_ptr<Ui::DynamicImage> fromUserpic,
|
||||
QString fromName);
|
||||
|
||||
[[nodiscard]] rpl::producer<> cancelInRequests() const;
|
||||
[[nodiscard]] rpl::producer<> cancelFromRequests() const;
|
||||
[[nodiscard]] rpl::producer<> changeFromRequests() const;
|
||||
[[nodiscard]] rpl::producer<ChatSearchTab> tabChanges() const;
|
||||
|
||||
private:
|
||||
struct Section {
|
||||
std::unique_ptr<Ui::RpWidget> outer;
|
||||
std::unique_ptr<Ui::IconButton> cancel;
|
||||
std::unique_ptr<Ui::PlainShadow> shadow;
|
||||
std::shared_ptr<Ui::DynamicImage> image;
|
||||
Ui::Text::String text;
|
||||
rpl::event_stream<> clicks;
|
||||
rpl::event_stream<> cancelRequests;
|
||||
bool subscribed = false;
|
||||
|
||||
void update();
|
||||
};
|
||||
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void showMenu();
|
||||
|
||||
void updateSection(
|
||||
not_null<Section*> section,
|
||||
std::shared_ptr<Ui::DynamicImage> image,
|
||||
TextWithEntities text);
|
||||
|
||||
Section _in;
|
||||
Section _from;
|
||||
rpl::variable<ChatSearchTab> _active;
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||
|
||||
std::vector<PossibleTab> _tabs;
|
||||
ChatSearchPeerTabType _peerTabType = ChatSearchPeerTabType::Chat;
|
||||
|
||||
};
|
||||
|
||||
enum class HashOrCashtag : uchar {
|
||||
None,
|
||||
Hashtag,
|
||||
Cashtag,
|
||||
};
|
||||
|
||||
struct FixedHashtagSearchQuery {
|
||||
QString text;
|
||||
int cursorPosition = 0;
|
||||
};
|
||||
[[nodiscard]] FixedHashtagSearchQuery FixHashtagSearchQuery(
|
||||
const QString &query,
|
||||
int cursorPosition,
|
||||
HashOrCashtag tag);
|
||||
|
||||
[[nodiscard]] HashOrCashtag IsHashOrCashtagSearchQuery(const QString &query);
|
||||
|
||||
} // namespace Dialogs
|
||||
1268
Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp
Normal file
1268
Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp
Normal file
File diff suppressed because it is too large
Load Diff
116
Telegram/SourceFiles/dialogs/ui/dialogs_layout.h
Normal file
116
Telegram/SourceFiles/dialogs/ui/dialogs_layout.h
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
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 "dialogs/ui/dialogs_quick_action_context.h"
|
||||
#include "ui/cached_round_corners.h"
|
||||
|
||||
namespace style {
|
||||
struct DialogRow;
|
||||
struct VerifiedBadge;
|
||||
} // namespace style
|
||||
|
||||
namespace st {
|
||||
extern const style::DialogRow &defaultDialogRow;
|
||||
} // namespace st
|
||||
|
||||
namespace Data {
|
||||
class Forum;
|
||||
class Folder;
|
||||
class Thread;
|
||||
} // namespace Data
|
||||
|
||||
namespace Dialogs {
|
||||
class Row;
|
||||
class FakeRow;
|
||||
class BasicRow;
|
||||
struct RightButton;
|
||||
} // namespace Dialogs
|
||||
|
||||
namespace Dialogs::Ui {
|
||||
|
||||
using namespace ::Ui;
|
||||
|
||||
class VideoUserpic;
|
||||
|
||||
struct TopicJumpCorners {
|
||||
CornersPixmaps normal;
|
||||
CornersPixmaps inverted;
|
||||
QPixmap small;
|
||||
int invertedRadius = 0;
|
||||
int smallKey = 0; // = `-radius` if top right else `radius`.
|
||||
};
|
||||
|
||||
struct TopicJumpCache {
|
||||
TopicJumpCorners corners;
|
||||
TopicJumpCorners over;
|
||||
TopicJumpCorners selected;
|
||||
TopicJumpCorners rippleMask;
|
||||
};
|
||||
|
||||
struct PaintContext {
|
||||
RightButton *rightButton = nullptr;
|
||||
std::vector<QImage*> *chatsFilterTags = nullptr;
|
||||
QuickActionContext *quickActionContext = nullptr;
|
||||
not_null<const style::DialogRow*> st;
|
||||
TopicJumpCache *topicJumpCache = nullptr;
|
||||
Data::Folder *folder = nullptr;
|
||||
Data::Forum *forum = nullptr;
|
||||
required<QBrush> currentBg;
|
||||
FilterId filter = 0;
|
||||
float64 topicsExpanded = 0.;
|
||||
crl::time now = 0;
|
||||
QStringView searchLowerText;
|
||||
int width = 0;
|
||||
bool active = false;
|
||||
bool selected = false;
|
||||
bool topicJumpSelected = false;
|
||||
bool paused = false;
|
||||
bool search = false;
|
||||
bool narrow = false;
|
||||
bool displayUnreadInfo = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] const style::icon *ChatTypeIcon(
|
||||
not_null<PeerData*> peer,
|
||||
const PaintContext &context);
|
||||
[[nodiscard]] const style::icon *ChatTypeIcon(not_null<PeerData*> peer);
|
||||
|
||||
[[nodiscard]] const style::VerifiedBadge &VerifiedStyle(
|
||||
const PaintContext &context);
|
||||
|
||||
class RowPainter {
|
||||
public:
|
||||
static void Paint(
|
||||
Painter &p,
|
||||
not_null<const Row*> row,
|
||||
VideoUserpic *videoUserpic,
|
||||
const PaintContext &context);
|
||||
static void Paint(
|
||||
Painter &p,
|
||||
not_null<const FakeRow*> row,
|
||||
const PaintContext &context);
|
||||
static QRect SendActionAnimationRect(
|
||||
not_null<const Data::Thread*> thread,
|
||||
FilterId filterId,
|
||||
QRect rect,
|
||||
int fullWidth,
|
||||
bool textUpdated);
|
||||
};
|
||||
|
||||
void PaintCollapsedRow(
|
||||
Painter &p,
|
||||
const BasicRow &row,
|
||||
Data::Folder *folder,
|
||||
const QString &text,
|
||||
int unread,
|
||||
const PaintContext &context);
|
||||
|
||||
int PaintRightButton(QPainter &p, const PaintContext &context);
|
||||
|
||||
} // namespace Dialogs::Ui
|
||||
612
Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp
Normal file
612
Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp
Normal file
@@ -0,0 +1,612 @@
|
||||
/*
|
||||
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 "dialogs/ui/dialogs_message_view.h"
|
||||
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "history/view/history_view_element.h"
|
||||
#include "history/view/history_view_item_preview.h"
|
||||
#include "main/main_session.h"
|
||||
#include "dialogs/dialogs_three_state_icon.h"
|
||||
#include "dialogs/ui/dialogs_layout.h"
|
||||
#include "dialogs/ui/dialogs_topics_view.h"
|
||||
#include "ui/effects/spoiler_mess.h"
|
||||
#include "ui/text/custom_emoji_helper.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/power_saving.h"
|
||||
#include "core/ui_integration.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "lang/lang_text_entity.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kEmojiLoopCount = 2;
|
||||
|
||||
template <ushort kTag>
|
||||
struct TextWithTagOffset {
|
||||
TextWithTagOffset(TextWithEntities text) : text(std::move(text)) {
|
||||
}
|
||||
TextWithTagOffset(QString text) : text({ std::move(text) }) {
|
||||
}
|
||||
static TextWithTagOffset FromString(const QString &text) {
|
||||
return { { text } };
|
||||
}
|
||||
|
||||
TextWithEntities text;
|
||||
int offset = -1;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace Lang {
|
||||
|
||||
template <ushort kTag>
|
||||
struct ReplaceTag<TextWithTagOffset<kTag>> {
|
||||
static TextWithTagOffset<kTag> Call(
|
||||
TextWithTagOffset<kTag> &&original,
|
||||
ushort tag,
|
||||
const TextWithTagOffset<kTag> &replacement);
|
||||
};
|
||||
|
||||
template <ushort kTag>
|
||||
TextWithTagOffset<kTag> ReplaceTag<TextWithTagOffset<kTag>>::Call(
|
||||
TextWithTagOffset<kTag> &&original,
|
||||
ushort tag,
|
||||
const TextWithTagOffset<kTag> &replacement) {
|
||||
const auto replacementPosition = FindTagReplacementPosition(
|
||||
original.text.text,
|
||||
tag);
|
||||
if (replacementPosition < 0) {
|
||||
return std::move(original);
|
||||
}
|
||||
original.text = ReplaceTag<TextWithEntities>::Replace(
|
||||
std::move(original.text),
|
||||
replacement.text,
|
||||
replacementPosition);
|
||||
if (tag == kTag) {
|
||||
original.offset = replacementPosition;
|
||||
} else if (original.offset > replacementPosition) {
|
||||
constexpr auto kReplaceCommandLength = 4;
|
||||
const auto replacementSize = replacement.text.text.size();
|
||||
original.offset += replacementSize - kReplaceCommandLength;
|
||||
}
|
||||
return std::move(original);
|
||||
}
|
||||
|
||||
} // namespace Lang
|
||||
|
||||
namespace Dialogs::Ui {
|
||||
|
||||
TextWithEntities DialogsPreviewText(TextWithEntities text) {
|
||||
auto result = Ui::Text::Filtered(
|
||||
std::move(text),
|
||||
{
|
||||
EntityType::Pre,
|
||||
EntityType::Code,
|
||||
EntityType::Spoiler,
|
||||
EntityType::StrikeOut,
|
||||
EntityType::Underline,
|
||||
EntityType::Italic,
|
||||
EntityType::CustomEmoji,
|
||||
EntityType::Colorized,
|
||||
});
|
||||
for (auto &entity : result.entities) {
|
||||
if (entity.type() == EntityType::Pre) {
|
||||
entity = EntityInText(
|
||||
EntityType::Code,
|
||||
entity.offset(),
|
||||
entity.length());
|
||||
} else if (entity.type() == EntityType::Colorized
|
||||
&& !entity.data().isEmpty()) {
|
||||
// Drop 'data' so that only link-color colorization takes place.
|
||||
entity = EntityInText(
|
||||
EntityType::Colorized,
|
||||
entity.offset(),
|
||||
entity.length());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
struct MessageView::LoadingContext {
|
||||
std::any context;
|
||||
rpl::lifetime lifetime;
|
||||
};
|
||||
|
||||
MessageView::MessageView()
|
||||
: _senderCache(st::dialogsTextWidthMin)
|
||||
, _textCache(st::dialogsTextWidthMin) {
|
||||
}
|
||||
|
||||
MessageView::~MessageView() = default;
|
||||
|
||||
void MessageView::itemInvalidated(not_null<const HistoryItem*> item) {
|
||||
if (_textCachedFor == item.get()) {
|
||||
_textCachedFor = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool MessageView::dependsOn(not_null<const HistoryItem*> item) const {
|
||||
return (_textCachedFor == item.get());
|
||||
}
|
||||
|
||||
bool MessageView::prepared(
|
||||
not_null<const HistoryItem*> item,
|
||||
Data::Forum *forum,
|
||||
Data::SavedMessages *monoforum) const {
|
||||
return (_textCachedFor == item.get())
|
||||
&& ((!forum && !monoforum)
|
||||
|| (_topics
|
||||
&& _topics->forum() == forum
|
||||
&& _topics->monoforum() == monoforum
|
||||
&& _topics->prepared()));
|
||||
}
|
||||
|
||||
void MessageView::prepare(
|
||||
not_null<const HistoryItem*> item,
|
||||
Data::Forum *forum,
|
||||
Data::SavedMessages *monoforum,
|
||||
Fn<void()> customEmojiRepaint,
|
||||
ToPreviewOptions options) {
|
||||
if (!forum && !monoforum) {
|
||||
_topics = nullptr;
|
||||
} else if (!_topics
|
||||
|| _topics->forum() != forum
|
||||
|| _topics->monoforum() != monoforum) {
|
||||
_topics = std::make_unique<TopicsView>(forum, monoforum);
|
||||
if (forum) {
|
||||
_topics->prepare(item->topicRootId(), customEmojiRepaint);
|
||||
} else {
|
||||
_topics->prepare(item->sublistPeerId(), customEmojiRepaint);
|
||||
}
|
||||
} else if (!_topics->prepared()) {
|
||||
if (forum) {
|
||||
_topics->prepare(item->topicRootId(), customEmojiRepaint);
|
||||
} else {
|
||||
_topics->prepare(item->sublistPeerId(), customEmojiRepaint);
|
||||
}
|
||||
}
|
||||
if (_textCachedFor == item.get()) {
|
||||
return;
|
||||
}
|
||||
options.existing = &_imagesCache;
|
||||
options.ignoreTopic = true;
|
||||
options.spoilerLoginCode = true;
|
||||
auto preview = item->toPreview(options);
|
||||
_leftIcon = (preview.icon == ItemPreview::Icon::ForwardedMessage)
|
||||
? &st::dialogsMiniForward
|
||||
: (preview.icon == ItemPreview::Icon::ReplyToStory)
|
||||
? &st::dialogsMiniReplyStory
|
||||
: nullptr;
|
||||
const auto hasImages = !preview.images.empty();
|
||||
const auto history = item->history();
|
||||
auto context = Core::TextContext({
|
||||
.session = &history->session(),
|
||||
.repaint = customEmojiRepaint,
|
||||
.customEmojiLoopLimit = kEmojiLoopCount,
|
||||
});
|
||||
const auto senderTill = (preview.arrowInTextPosition > 0)
|
||||
? preview.arrowInTextPosition
|
||||
: preview.imagesInTextPosition;
|
||||
if ((hasImages || _leftIcon) && senderTill > 0) {
|
||||
auto sender = Text::Mid(preview.text, 0, senderTill);
|
||||
TextUtilities::Trim(sender);
|
||||
_senderCache.setMarkedText(
|
||||
st::dialogsTextStyle,
|
||||
std::move(sender),
|
||||
DialogTextOptions());
|
||||
preview.text = Text::Mid(preview.text, senderTill);
|
||||
} else {
|
||||
_senderCache = { st::dialogsTextWidthMin };
|
||||
}
|
||||
TextUtilities::Trim(preview.text);
|
||||
auto textToCache = DialogsPreviewText(std::move(preview.text));
|
||||
|
||||
if (!options.searchLowerText.isEmpty()) {
|
||||
static constexpr auto kLeftShift = 15;
|
||||
auto minFrom = std::numeric_limits<uint16>::max();
|
||||
|
||||
const auto words = Ui::Text::Words(options.searchLowerText);
|
||||
textToCache.entities.reserve(textToCache.entities.size()
|
||||
+ words.size());
|
||||
|
||||
for (const auto &word : words) {
|
||||
const auto selection = HistoryView::FindSearchQueryHighlight(
|
||||
textToCache.text,
|
||||
word);
|
||||
if (!selection.empty()) {
|
||||
minFrom = std::min(minFrom, selection.from);
|
||||
textToCache.entities.push_back(EntityInText{
|
||||
EntityType::Colorized,
|
||||
selection.from,
|
||||
selection.to - selection.from
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (minFrom == std::numeric_limits<uint16>::max()
|
||||
&& !item->replyTo().quote.empty()) {
|
||||
auto textQuote = TextWithEntities();
|
||||
for (const auto &word : words) {
|
||||
const auto selection = HistoryView::FindSearchQueryHighlight(
|
||||
item->replyTo().quote.text,
|
||||
word);
|
||||
if (!selection.empty()) {
|
||||
minFrom = 0;
|
||||
if (textQuote.empty()) {
|
||||
textQuote = item->replyTo().quote;
|
||||
}
|
||||
textQuote.entities.push_back(EntityInText{
|
||||
EntityType::Colorized,
|
||||
selection.from,
|
||||
selection.to - selection.from
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!textQuote.empty()) {
|
||||
auto helper = Ui::Text::CustomEmojiHelper(context);
|
||||
const auto factory = Ui::Text::PaletteDependentEmoji{
|
||||
.factory = [=] {
|
||||
const auto &icon = st::dialogsMiniQuoteIcon;
|
||||
auto image = QImage(
|
||||
icon.size() * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
image.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
image.fill(Qt::transparent);
|
||||
{
|
||||
auto p = Painter(&image);
|
||||
icon.paintInCenter(
|
||||
p,
|
||||
Rect(icon.size()),
|
||||
st::dialogsTextFg->c);
|
||||
}
|
||||
return image;
|
||||
},
|
||||
.margin = QMargins(
|
||||
st::lineWidth * 2,
|
||||
0,
|
||||
st::lineWidth * 2,
|
||||
0),
|
||||
};
|
||||
textToCache = textQuote
|
||||
.append(helper.paletteDependent(factory))
|
||||
.append(std::move(textToCache));
|
||||
context = helper.context(customEmojiRepaint);
|
||||
}
|
||||
}
|
||||
|
||||
if (!words.empty() && minFrom != std::numeric_limits<uint16>::max()) {
|
||||
std::sort(
|
||||
textToCache.entities.begin(),
|
||||
textToCache.entities.end(),
|
||||
[](const auto &a, const auto &b) {
|
||||
return a.offset() < b.offset();
|
||||
});
|
||||
|
||||
const auto textSize = textToCache.text.size();
|
||||
minFrom = (minFrom > textSize || minFrom < kLeftShift)
|
||||
? 0
|
||||
: minFrom - kLeftShift;
|
||||
|
||||
textToCache = (TextWithEntities{
|
||||
minFrom > 0 ? kQEllipsis : QString()
|
||||
}).append(Text::Mid(std::move(textToCache), minFrom));
|
||||
}
|
||||
}
|
||||
_hasPlainLinkAtBegin = !textToCache.entities.empty()
|
||||
&& (textToCache.entities.front().type() == EntityType::Colorized);
|
||||
_textCache.setMarkedText(
|
||||
st::dialogsTextStyle,
|
||||
std::move(textToCache),
|
||||
DialogTextOptions(),
|
||||
std::move(context));
|
||||
_textCachedFor = item;
|
||||
_imagesCache = std::move(preview.images);
|
||||
if (!ranges::any_of(_imagesCache, &ItemPreviewImage::hasSpoiler)) {
|
||||
_spoiler = nullptr;
|
||||
} else if (!_spoiler) {
|
||||
_spoiler = std::make_unique<SpoilerAnimation>(customEmojiRepaint);
|
||||
}
|
||||
if (preview.loadingContext.has_value()) {
|
||||
if (!_loadingContext) {
|
||||
_loadingContext = std::make_unique<LoadingContext>();
|
||||
item->history()->session().downloaderTaskFinished(
|
||||
) | rpl::on_next([=] {
|
||||
_textCachedFor = nullptr;
|
||||
}, _loadingContext->lifetime);
|
||||
}
|
||||
_loadingContext->context = std::move(preview.loadingContext);
|
||||
} else {
|
||||
_loadingContext = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool MessageView::isInTopicJump(int x, int y) const {
|
||||
return _topics && _topics->isInTopicJumpArea(x, y);
|
||||
}
|
||||
|
||||
void MessageView::addTopicJumpRipple(
|
||||
QPoint origin,
|
||||
not_null<TopicJumpCache*> topicJumpCache,
|
||||
Fn<void()> updateCallback) {
|
||||
if (_topics) {
|
||||
_topics->addTopicJumpRipple(
|
||||
origin,
|
||||
topicJumpCache,
|
||||
std::move(updateCallback));
|
||||
}
|
||||
}
|
||||
|
||||
void MessageView::stopLastRipple() {
|
||||
if (_topics) {
|
||||
_topics->stopLastRipple();
|
||||
}
|
||||
}
|
||||
|
||||
void MessageView::clearRipple() {
|
||||
if (_topics) {
|
||||
_topics->clearRipple();
|
||||
}
|
||||
}
|
||||
|
||||
int MessageView::countWidth() const {
|
||||
auto result = 0;
|
||||
if (!_senderCache.isEmpty()) {
|
||||
result += _senderCache.maxWidth();
|
||||
if (!_imagesCache.empty() && !_leftIcon) {
|
||||
result += st::dialogsMiniPreviewSkip
|
||||
+ st::dialogsMiniPreviewRight;
|
||||
}
|
||||
}
|
||||
if (_leftIcon) {
|
||||
const auto w = _leftIcon->icon.icon.width();
|
||||
result += w
|
||||
+ (_imagesCache.empty()
|
||||
? _leftIcon->skipText
|
||||
: _leftIcon->skipMedia);
|
||||
}
|
||||
if (!_imagesCache.empty()) {
|
||||
result += (_imagesCache.size()
|
||||
* (st::dialogsMiniPreview + st::dialogsMiniPreviewSkip))
|
||||
+ st::dialogsMiniPreviewRight;
|
||||
}
|
||||
return result + _textCache.maxWidth();
|
||||
}
|
||||
|
||||
void MessageView::paint(
|
||||
Painter &p,
|
||||
const QRect &geometry,
|
||||
const PaintContext &context) const {
|
||||
if (geometry.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
p.setFont(st::dialogsTextFont);
|
||||
p.setPen(context.active
|
||||
? st::dialogsTextFgActive
|
||||
: context.selected
|
||||
? st::dialogsTextFgOver
|
||||
: st::dialogsTextFg);
|
||||
const auto withTopic = _topics && context.st->topicsHeight;
|
||||
const auto palette = &(withTopic
|
||||
? (context.active
|
||||
? st::dialogsTextPaletteInTopicActive
|
||||
: context.selected
|
||||
? st::dialogsTextPaletteInTopicOver
|
||||
: st::dialogsTextPaletteInTopic)
|
||||
: (context.active
|
||||
? st::dialogsTextPaletteActive
|
||||
: context.selected
|
||||
? st::dialogsTextPaletteOver
|
||||
: st::dialogsTextPalette));
|
||||
|
||||
auto rect = geometry;
|
||||
const auto checkJump = withTopic && !context.active;
|
||||
const auto jump1 = checkJump ? _topics->jumpToTopicWidth() : 0;
|
||||
if (jump1) {
|
||||
paintJumpToLast(p, rect, context, jump1);
|
||||
} else if (_topics) {
|
||||
_topics->clearTopicJumpGeometry();
|
||||
}
|
||||
|
||||
if (withTopic) {
|
||||
_topics->paint(p, rect, context);
|
||||
rect.setTop(rect.top() + context.st->topicsHeight);
|
||||
}
|
||||
|
||||
auto finalRight = rect.x() + rect.width();
|
||||
if (jump1) {
|
||||
rect.setWidth(rect.width() - st::forumDialogJumpArrowSkip);
|
||||
finalRight -= st::forumDialogJumpArrowSkip;
|
||||
}
|
||||
const auto pausedSpoiler = context.paused
|
||||
|| On(PowerSaving::kChatSpoiler);
|
||||
if (!_senderCache.isEmpty()) {
|
||||
_senderCache.draw(p, {
|
||||
.position = rect.topLeft(),
|
||||
.availableWidth = rect.width(),
|
||||
.palette = palette,
|
||||
.elisionHeight = rect.height(),
|
||||
});
|
||||
rect.setLeft(rect.x() + _senderCache.maxWidth());
|
||||
if (!_imagesCache.empty() && !_leftIcon) {
|
||||
const auto skip = st::dialogsMiniPreviewSkip
|
||||
+ st::dialogsMiniPreviewRight;
|
||||
rect.setLeft(rect.x() + skip);
|
||||
}
|
||||
}
|
||||
|
||||
if (_leftIcon) {
|
||||
const auto &icon = ThreeStateIcon(
|
||||
_leftIcon->icon,
|
||||
context.active,
|
||||
context.selected);
|
||||
const auto w = (icon.width());
|
||||
if (rect.width() > w) {
|
||||
if (_hasPlainLinkAtBegin && !context.active) {
|
||||
icon.paint(
|
||||
p,
|
||||
rect.topLeft(),
|
||||
rect.width(),
|
||||
palette->linkFg->c);
|
||||
} else {
|
||||
icon.paint(p, rect.topLeft(), rect.width());
|
||||
}
|
||||
rect.setLeft(rect.x()
|
||||
+ w
|
||||
+ (_imagesCache.empty()
|
||||
? _leftIcon->skipText
|
||||
: _leftIcon->skipMedia));
|
||||
}
|
||||
}
|
||||
for (const auto &image : _imagesCache) {
|
||||
const auto w = st::dialogsMiniPreview + st::dialogsMiniPreviewSkip;
|
||||
if (rect.width() < w) {
|
||||
break;
|
||||
}
|
||||
const auto mini = QRect(
|
||||
rect.x(),
|
||||
rect.y() + st::dialogsMiniPreviewTop,
|
||||
st::dialogsMiniPreview,
|
||||
st::dialogsMiniPreview);
|
||||
if (!image.data.isNull()) {
|
||||
p.drawImage(mini, image.data);
|
||||
if (image.hasSpoiler()) {
|
||||
const auto frame = DefaultImageSpoiler().frame(
|
||||
_spoiler->index(context.now, pausedSpoiler));
|
||||
if (image.isEllipse()) {
|
||||
const auto radius = st::dialogsMiniPreview / 2;
|
||||
static auto mask = Images::CornersMask(radius);
|
||||
FillSpoilerRect(
|
||||
p,
|
||||
mini,
|
||||
Images::CornersMaskRef(mask),
|
||||
frame,
|
||||
_cornersCache);
|
||||
} else {
|
||||
FillSpoilerRect(p, mini, frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
rect.setLeft(rect.x() + w);
|
||||
}
|
||||
if (!_imagesCache.empty()) {
|
||||
rect.setLeft(rect.x() + st::dialogsMiniPreviewRight);
|
||||
}
|
||||
// Style of _textCache.
|
||||
static const auto ellipsisWidth = st::dialogsTextStyle.font->width(
|
||||
kQEllipsis);
|
||||
if (rect.width() > ellipsisWidth) {
|
||||
_textCache.draw(p, {
|
||||
.position = rect.topLeft(),
|
||||
.availableWidth = rect.width(),
|
||||
.palette = palette,
|
||||
.spoiler = Text::DefaultSpoilerCache(),
|
||||
.now = context.now,
|
||||
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
|
||||
.pausedSpoiler = pausedSpoiler,
|
||||
.elisionHeight = rect.height(),
|
||||
});
|
||||
rect.setLeft(rect.x() + _textCache.maxWidth());
|
||||
}
|
||||
if (jump1) {
|
||||
const auto position = st::forumDialogJumpArrowPosition
|
||||
+ QPoint((rect.width() > 0) ? rect.x() : finalRight, rect.y());
|
||||
(context.selected
|
||||
? st::forumDialogJumpArrowOver
|
||||
: st::forumDialogJumpArrow).paint(p, position, context.width);
|
||||
}
|
||||
}
|
||||
|
||||
void MessageView::paintJumpToLast(
|
||||
Painter &p,
|
||||
const QRect &rect,
|
||||
const PaintContext &context,
|
||||
int width1) const {
|
||||
if (!context.topicJumpCache) {
|
||||
_topics->clearTopicJumpGeometry();
|
||||
return;
|
||||
}
|
||||
const auto width2 = countWidth() + st::forumDialogJumpArrowSkip;
|
||||
const auto geometry = FillJumpToLastBg(p, {
|
||||
.st = context.st,
|
||||
.corners = (context.selected
|
||||
? &context.topicJumpCache->over
|
||||
: &context.topicJumpCache->corners),
|
||||
.geometry = rect,
|
||||
.bg = (context.selected
|
||||
? st::dialogsRippleBg
|
||||
: st::dialogsBgOver),
|
||||
.width1 = width1,
|
||||
.width2 = width2,
|
||||
});
|
||||
if (context.topicJumpSelected) {
|
||||
p.setOpacity(0.1);
|
||||
FillJumpToLastPrepared(p, {
|
||||
.st = context.st,
|
||||
.corners = &context.topicJumpCache->selected,
|
||||
.bg = st::dialogsTextFg,
|
||||
.prepared = geometry,
|
||||
});
|
||||
p.setOpacity(1.);
|
||||
}
|
||||
if (!_topics->changeTopicJumpGeometry(geometry)) {
|
||||
auto color = st::dialogsTextFg->c;
|
||||
color.setAlpha(color.alpha() / 10);
|
||||
if (color.alpha() > 0) {
|
||||
_topics->paintRipple(p, 0, 0, context.width, &color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HistoryView::ItemPreview PreviewWithSender(
|
||||
HistoryView::ItemPreview &&preview,
|
||||
const QString &sender,
|
||||
TextWithEntities topic) {
|
||||
const auto wrappedSender = st::wrap_rtl(sender);
|
||||
auto senderWithOffset = topic.empty()
|
||||
? TextWithTagOffset<lt_from>::FromString(wrappedSender)
|
||||
: tr::lng_dialogs_text_from_in_topic(
|
||||
tr::now,
|
||||
lt_from,
|
||||
{ wrappedSender },
|
||||
lt_topic,
|
||||
std::move(topic),
|
||||
TextWithTagOffset<lt_from>::FromString);
|
||||
auto wrappedWithOffset = tr::lng_dialogs_text_from_wrapped(
|
||||
tr::now,
|
||||
lt_from,
|
||||
std::move(senderWithOffset.text),
|
||||
TextWithTagOffset<lt_from>::FromString);
|
||||
const auto wrappedSize = wrappedWithOffset.text.text.size();
|
||||
auto fullWithOffset = tr::lng_dialogs_text_with_from(
|
||||
tr::now,
|
||||
lt_from_part,
|
||||
Ui::Text::Colorized(std::move(wrappedWithOffset.text)),
|
||||
lt_message,
|
||||
std::move(preview.text),
|
||||
TextWithTagOffset<lt_from_part>::FromString);
|
||||
preview.text = std::move(fullWithOffset.text);
|
||||
preview.arrowInTextPosition = (fullWithOffset.offset < 0
|
||||
|| wrappedWithOffset.offset < 0
|
||||
|| senderWithOffset.offset < 0)
|
||||
? -1
|
||||
: (fullWithOffset.offset
|
||||
+ wrappedWithOffset.offset
|
||||
+ senderWithOffset.offset
|
||||
+ sender.size());
|
||||
preview.imagesInTextPosition = (fullWithOffset.offset < 0)
|
||||
? 0
|
||||
: (fullWithOffset.offset + wrappedSize);
|
||||
return std::move(preview);
|
||||
}
|
||||
|
||||
} // namespace Dialogs::Ui
|
||||
110
Telegram/SourceFiles/dialogs/ui/dialogs_message_view.h
Normal file
110
Telegram/SourceFiles/dialogs/ui/dialogs_message_view.h
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
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 <any>
|
||||
|
||||
class Image;
|
||||
class HistoryItem;
|
||||
enum class ImageRoundRadius;
|
||||
|
||||
namespace style {
|
||||
struct DialogRow;
|
||||
struct DialogsMiniIcon;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
class SpoilerAnimation;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Data {
|
||||
class Forum;
|
||||
class SavedMessages;
|
||||
} // namespace Data
|
||||
|
||||
namespace HistoryView {
|
||||
struct ToPreviewOptions;
|
||||
struct ItemPreviewImage;
|
||||
struct ItemPreview;
|
||||
} // namespace HistoryView
|
||||
|
||||
namespace Dialogs::Ui {
|
||||
|
||||
using namespace ::Ui;
|
||||
|
||||
struct PaintContext;
|
||||
struct TopicJumpCache;
|
||||
class TopicsView;
|
||||
|
||||
[[nodiscard]] TextWithEntities DialogsPreviewText(TextWithEntities text);
|
||||
|
||||
class MessageView final {
|
||||
public:
|
||||
MessageView();
|
||||
~MessageView();
|
||||
|
||||
using ToPreviewOptions = HistoryView::ToPreviewOptions;
|
||||
using ItemPreviewImage = HistoryView::ItemPreviewImage;
|
||||
using ItemPreview = HistoryView::ItemPreview;
|
||||
|
||||
void itemInvalidated(not_null<const HistoryItem*> item);
|
||||
[[nodiscard]] bool dependsOn(not_null<const HistoryItem*> item) const;
|
||||
|
||||
[[nodiscard]] bool prepared(
|
||||
not_null<const HistoryItem*> item,
|
||||
Data::Forum *forum,
|
||||
Data::SavedMessages *monoforum) const;
|
||||
void prepare(
|
||||
not_null<const HistoryItem*> item,
|
||||
Data::Forum *forum,
|
||||
Data::SavedMessages *monoforum,
|
||||
Fn<void()> customEmojiRepaint,
|
||||
ToPreviewOptions options);
|
||||
|
||||
void paint(
|
||||
Painter &p,
|
||||
const QRect &geometry,
|
||||
const PaintContext &context) const;
|
||||
|
||||
[[nodiscard]] bool isInTopicJump(int x, int y) const;
|
||||
void addTopicJumpRipple(
|
||||
QPoint origin,
|
||||
not_null<TopicJumpCache*> topicJumpCache,
|
||||
Fn<void()> updateCallback);
|
||||
void stopLastRipple();
|
||||
void clearRipple();
|
||||
|
||||
private:
|
||||
struct LoadingContext;
|
||||
|
||||
[[nodiscard]] int countWidth() const;
|
||||
void paintJumpToLast(
|
||||
Painter &p,
|
||||
const QRect &rect,
|
||||
const PaintContext &context,
|
||||
int width1) const;
|
||||
|
||||
mutable const HistoryItem *_textCachedFor = nullptr;
|
||||
mutable Text::String _senderCache;
|
||||
mutable std::unique_ptr<TopicsView> _topics;
|
||||
mutable Text::String _textCache;
|
||||
mutable std::vector<ItemPreviewImage> _imagesCache;
|
||||
mutable std::unique_ptr<SpoilerAnimation> _spoiler;
|
||||
mutable std::unique_ptr<LoadingContext> _loadingContext;
|
||||
mutable const style::DialogsMiniIcon *_leftIcon = nullptr;
|
||||
mutable QImage _cornersCache;
|
||||
mutable bool _hasPlainLinkAtBegin = false;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] HistoryView::ItemPreview PreviewWithSender(
|
||||
HistoryView::ItemPreview &&preview,
|
||||
const QString &sender,
|
||||
TextWithEntities topic);
|
||||
|
||||
} // namespace Dialogs::Ui
|
||||
23
Telegram/SourceFiles/dialogs/ui/dialogs_quick_action.h
Normal file
23
Telegram/SourceFiles/dialogs/ui/dialogs_quick_action.h
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
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 Dialogs::Ui {
|
||||
|
||||
using namespace ::Ui;
|
||||
|
||||
enum class QuickDialogAction {
|
||||
Mute,
|
||||
Pin,
|
||||
Read,
|
||||
Archive,
|
||||
Delete,
|
||||
Disabled,
|
||||
};
|
||||
|
||||
} // namespace Dialogs::Ui
|
||||
@@ -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
|
||||
|
||||
#include "dialogs/ui/dialogs_quick_action.h"
|
||||
#include "ui/controls/swipe_handler_data.h"
|
||||
|
||||
namespace Lottie {
|
||||
class Icon;
|
||||
} // namespace Lottie
|
||||
|
||||
namespace Ui {
|
||||
class RippleAnimation;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Dialogs::Ui {
|
||||
|
||||
using namespace ::Ui;
|
||||
|
||||
enum class QuickDialogActionLabel {
|
||||
Mute,
|
||||
Unmute,
|
||||
Pin,
|
||||
Unpin,
|
||||
Read,
|
||||
Unread,
|
||||
Archive,
|
||||
Unarchive,
|
||||
Delete,
|
||||
Disabled,
|
||||
};
|
||||
|
||||
struct QuickActionContext {
|
||||
::Ui::Controls::SwipeContextData data;
|
||||
std::unique_ptr<Lottie::Icon> icon;
|
||||
std::unique_ptr<Ui::RippleAnimation> ripple;
|
||||
std::unique_ptr<Ui::RippleAnimation> rippleFg;
|
||||
QuickDialogAction action;
|
||||
crl::time finishedAt = 0;
|
||||
};
|
||||
|
||||
} // namespace Dialogs::Ui
|
||||
278
Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
Normal file
278
Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp
Normal file
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
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 "dialogs/ui/dialogs_stories_content.h"
|
||||
|
||||
#include "base/unixtime.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_document_media.h"
|
||||
#include "data/data_file_origin.h"
|
||||
#include "data/data_photo.h"
|
||||
#include "data/data_photo_media.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_stories.h"
|
||||
#include "data/data_user.h"
|
||||
#include "dialogs/ui/dialogs_stories_list.h"
|
||||
#include "info/stories/info_stories_widget.h"
|
||||
#include "info/info_controller.h"
|
||||
#include "info/info_memento.h"
|
||||
#include "main/main_session.h"
|
||||
#include "media/stories/media_stories_stealth.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/dynamic_image.h"
|
||||
#include "ui/dynamic_thumbnails.h"
|
||||
#include "ui/painter.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
#include "styles/style_media_stories.h"
|
||||
|
||||
namespace Dialogs::Stories {
|
||||
namespace {
|
||||
|
||||
constexpr auto kShownLastCount = 3;
|
||||
|
||||
class State final {
|
||||
public:
|
||||
State(not_null<Data::Stories*> data, Data::StorySourcesList list);
|
||||
|
||||
[[nodiscard]] Content next();
|
||||
|
||||
private:
|
||||
const not_null<Data::Stories*> _data;
|
||||
const Data::StorySourcesList _list;
|
||||
base::flat_map<
|
||||
not_null<PeerData*>,
|
||||
std::shared_ptr<Ui::DynamicImage>> _userpics;
|
||||
|
||||
};
|
||||
|
||||
State::State(not_null<Data::Stories*> data, Data::StorySourcesList list)
|
||||
: _data(data)
|
||||
, _list(list) {
|
||||
}
|
||||
|
||||
Content State::next() {
|
||||
const auto &sources = _data->sources(_list);
|
||||
auto result = Content{ .total = int(sources.size()) };
|
||||
result.elements.reserve(sources.size());
|
||||
for (const auto &info : sources) {
|
||||
const auto source = _data->source(info.id);
|
||||
Assert(source != nullptr);
|
||||
|
||||
auto userpic = std::shared_ptr<Ui::DynamicImage>();
|
||||
const auto peer = source->peer;
|
||||
if (const auto i = _userpics.find(peer); i != end(_userpics)) {
|
||||
userpic = i->second;
|
||||
} else {
|
||||
userpic = Ui::MakeUserpicThumbnail(peer, true);
|
||||
_userpics.emplace(peer, userpic);
|
||||
}
|
||||
result.elements.push_back({
|
||||
.id = uint64(peer->id.value),
|
||||
.name = peer->shortName(),
|
||||
.thumbnail = std::move(userpic),
|
||||
.count = info.count,
|
||||
.unreadCount = info.unreadCount,
|
||||
.hasVideoStream = info.hasVideoStream ? 1U : 0U,
|
||||
.skipSmall = peer->isSelf() ? 1U : 0U,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
rpl::producer<Content> ContentForSession(
|
||||
not_null<Main::Session*> session,
|
||||
Data::StorySourcesList list) {
|
||||
return [=](auto consumer) {
|
||||
auto result = rpl::lifetime();
|
||||
const auto stories = &session->data().stories();
|
||||
const auto state = result.make_state<State>(stories, list);
|
||||
rpl::single(
|
||||
rpl::empty
|
||||
) | rpl::then(
|
||||
stories->sourcesChanged(list)
|
||||
) | rpl::on_next([=] {
|
||||
consumer.put_next(state->next());
|
||||
}, result);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
rpl::producer<Content> LastForPeer(not_null<PeerData*> peer) {
|
||||
using namespace rpl::mappers;
|
||||
|
||||
const auto stories = &peer->owner().stories();
|
||||
const auto peerId = peer->id;
|
||||
|
||||
return rpl::single(
|
||||
peerId
|
||||
) | rpl::then(
|
||||
stories->sourceChanged() | rpl::filter(_1 == peerId)
|
||||
) | rpl::map([=] {
|
||||
auto ids = std::vector<StoryId>();
|
||||
auto readTill = StoryId();
|
||||
auto total = 0;
|
||||
if (const auto source = stories->source(peerId)) {
|
||||
readTill = source->readTill;
|
||||
total = int(source->ids.size());
|
||||
ids = ranges::views::all(source->ids)
|
||||
| ranges::views::reverse
|
||||
| ranges::views::take(kShownLastCount)
|
||||
| ranges::views::transform(&Data::StoryIdDates::id)
|
||||
| ranges::to_vector;
|
||||
}
|
||||
return rpl::make_producer<Content>([=](auto consumer) {
|
||||
auto lifetime = rpl::lifetime();
|
||||
if (ids.empty()) {
|
||||
consumer.put_next(Content());
|
||||
consumer.put_done();
|
||||
return lifetime;
|
||||
}
|
||||
|
||||
struct State {
|
||||
Fn<void()> check;
|
||||
base::has_weak_ptr guard;
|
||||
int readTill = StoryId();
|
||||
bool pushed = false;
|
||||
};
|
||||
const auto state = lifetime.make_state<State>();
|
||||
state->readTill = readTill;
|
||||
state->check = [=] {
|
||||
if (state->pushed) {
|
||||
return;
|
||||
}
|
||||
auto done = true;
|
||||
auto resolving = false;
|
||||
auto result = Content{ .total = total };
|
||||
for (const auto id : ids) {
|
||||
const auto storyId = FullStoryId{ peerId, id };
|
||||
const auto maybe = stories->lookup(storyId);
|
||||
if (maybe) {
|
||||
if (!resolving) {
|
||||
const auto stream = (*maybe)->call();
|
||||
const auto unread = stream
|
||||
|| (id > state->readTill);
|
||||
result.elements.reserve(ids.size());
|
||||
result.elements.push_back({
|
||||
.id = uint64(id),
|
||||
.thumbnail = Ui::MakeStoryThumbnail(*maybe),
|
||||
.count = 1U,
|
||||
.unreadCount = unread ? 1U : 0U,
|
||||
.hasVideoStream = stream ? 1U : 0U,
|
||||
});
|
||||
if (unread) {
|
||||
done = false;
|
||||
}
|
||||
}
|
||||
} else if (maybe.error() == Data::NoStory::Unknown) {
|
||||
resolving = true;
|
||||
stories->resolve(
|
||||
storyId,
|
||||
crl::guard(&state->guard, state->check));
|
||||
}
|
||||
}
|
||||
if (resolving) {
|
||||
return;
|
||||
}
|
||||
state->pushed = true;
|
||||
consumer.put_next(std::move(result));
|
||||
if (done) {
|
||||
consumer.put_done();
|
||||
}
|
||||
};
|
||||
|
||||
rpl::single(peerId) | rpl::then(
|
||||
stories->itemsChanged() | rpl::filter(_1 == peerId)
|
||||
) | rpl::on_next(state->check, lifetime);
|
||||
|
||||
stories->session().changes().storyUpdates(
|
||||
Data::StoryUpdate::Flag::MarkRead
|
||||
) | rpl::on_next([=](const Data::StoryUpdate &update) {
|
||||
if (update.story->peer()->id == peerId) {
|
||||
if (update.story->id() > state->readTill) {
|
||||
state->readTill = update.story->id();
|
||||
if (ranges::contains(ids, state->readTill)
|
||||
|| state->readTill > ids.front()) {
|
||||
state->pushed = false;
|
||||
state->check();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, lifetime);
|
||||
|
||||
return lifetime;
|
||||
});
|
||||
}) | rpl::flatten_latest();
|
||||
}
|
||||
|
||||
void FillSourceMenu(
|
||||
not_null<Window::SessionController*> controller,
|
||||
const ShowMenuRequest &request) {
|
||||
const auto owner = &controller->session().data();
|
||||
const auto peer = owner->peer(PeerId(request.id));
|
||||
const auto &add = request.callback;
|
||||
if (peer->isSelf()) {
|
||||
add(tr::lng_stories_archive_button(tr::now), [=] {
|
||||
controller->showSection(Info::Stories::Make(
|
||||
peer,
|
||||
Info::Stories::ArchiveId()));
|
||||
}, &st::menuIconStoriesArchiveSection);
|
||||
add(tr::lng_stories_my_title(tr::now), [=] {
|
||||
controller->showSection(Info::Stories::Make(peer));
|
||||
}, &st::menuIconStoriesSavedSection);
|
||||
} else {
|
||||
const auto group = peer->isMegagroup();
|
||||
const auto channel = peer->isChannel();
|
||||
const auto showHistoryText = group
|
||||
? tr::lng_context_open_group(tr::now)
|
||||
: channel
|
||||
? tr::lng_context_open_channel(tr::now)
|
||||
: tr::lng_profile_send_message(tr::now);
|
||||
add(showHistoryText, [=] {
|
||||
controller->showPeerHistory(peer);
|
||||
}, channel ? &st::menuIconChannel : &st::menuIconChatBubble);
|
||||
const auto viewProfileText = group
|
||||
? tr::lng_context_view_group(tr::now)
|
||||
: channel
|
||||
? tr::lng_context_view_channel(tr::now)
|
||||
: tr::lng_context_view_profile(tr::now);
|
||||
add(viewProfileText, [=] {
|
||||
controller->showPeerInfo(peer);
|
||||
}, channel ? &st::menuIconInfo : &st::menuIconProfile);
|
||||
if (!peer->hasActiveVideoStream() && peer->hasUnreadStories()) {
|
||||
Media::Stories::AddStealthModeMenu(add, peer, controller);
|
||||
}
|
||||
const auto in = [&](Data::StorySourcesList list) {
|
||||
return ranges::contains(
|
||||
owner->stories().sources(list),
|
||||
peer->id,
|
||||
&Data::StoriesSourceInfo::id);
|
||||
};
|
||||
const auto toggle = [=](bool shown) {
|
||||
owner->stories().toggleHidden(
|
||||
peer->id,
|
||||
!shown,
|
||||
controller->uiShow());
|
||||
};
|
||||
if (in(Data::StorySourcesList::NotHidden)) {
|
||||
add(tr::lng_stories_archive(tr::now), [=] {
|
||||
toggle(false);
|
||||
}, &st::menuIconArchive);
|
||||
}
|
||||
if (in(Data::StorySourcesList::Hidden)) {
|
||||
add(tr::lng_stories_unarchive(tr::now), [=] {
|
||||
toggle(true);
|
||||
}, &st::menuIconUnarchive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Dialogs::Stories
|
||||
38
Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h
Normal file
38
Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.h
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
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 {
|
||||
enum class StorySourcesList : uchar;
|
||||
class Story;
|
||||
} // namespace Data
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace Dialogs::Stories {
|
||||
|
||||
struct Content;
|
||||
struct ShowMenuRequest;
|
||||
|
||||
[[nodiscard]] rpl::producer<Content> ContentForSession(
|
||||
not_null<Main::Session*> session,
|
||||
Data::StorySourcesList list);
|
||||
|
||||
[[nodiscard]] rpl::producer<Content> LastForPeer(not_null<PeerData*> peer);
|
||||
|
||||
void FillSourceMenu(
|
||||
not_null<Window::SessionController*> controller,
|
||||
const ShowMenuRequest &request);
|
||||
|
||||
} // namespace Dialogs::Stories
|
||||
1249
Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
Normal file
1249
Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp
Normal file
File diff suppressed because it is too large
Load Diff
223
Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
Normal file
223
Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h
Normal file
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
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 "base/weak_ptr.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/text/text_custom_emoji.h"
|
||||
#include "ui/widgets/menu/menu_add_action_callback.h"
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
class QPainter;
|
||||
|
||||
namespace style {
|
||||
struct DialogsStories;
|
||||
struct DialogsStoriesList;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
class PopupMenu;
|
||||
class DynamicImage;
|
||||
struct OutlineSegment;
|
||||
class ImportantTooltip;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Dialogs::Stories {
|
||||
|
||||
struct Element {
|
||||
uint64 id = 0;
|
||||
QString name;
|
||||
std::shared_ptr<Ui::DynamicImage> thumbnail;
|
||||
uint32 count : 15 = 0;
|
||||
uint32 unreadCount : 15 = 0;
|
||||
uint32 hasVideoStream : 1 = 0;
|
||||
uint32 skipSmall : 1 = 0;
|
||||
|
||||
friend inline bool operator==(
|
||||
const Element &a,
|
||||
const Element &b) = default;
|
||||
};
|
||||
|
||||
struct Content {
|
||||
std::vector<Element> elements;
|
||||
int total = 0;
|
||||
|
||||
friend inline bool operator==(
|
||||
const Content &a,
|
||||
const Content &b) = default;
|
||||
};
|
||||
|
||||
struct ShowMenuRequest {
|
||||
uint64 id = 0;
|
||||
Ui::Menu::MenuCallback callback;
|
||||
};
|
||||
|
||||
class List final : public Ui::RpWidget {
|
||||
public:
|
||||
List(
|
||||
not_null<QWidget*> parent,
|
||||
const style::DialogsStoriesList &st,
|
||||
rpl::producer<Content> content);
|
||||
~List();
|
||||
|
||||
void setExpandedHeight(int height, bool momentum = false);
|
||||
void setLayoutConstraints(
|
||||
QPoint positionSmall,
|
||||
style::align alignSmall,
|
||||
QRect geometryFull = QRect());
|
||||
void setShowTooltip(
|
||||
not_null<Ui::RpWidget*> tooltipParent,
|
||||
rpl::producer<bool> shown,
|
||||
Fn<void()> hide);
|
||||
void raiseTooltip();
|
||||
|
||||
struct CollapsedGeometry {
|
||||
QRect geometry;
|
||||
float64 expanded = 0.;
|
||||
float64 singleWidth = 0.;
|
||||
};
|
||||
[[nodiscard]] CollapsedGeometry collapsedGeometryCurrent() const;
|
||||
[[nodiscard]] rpl::producer<> collapsedGeometryChanged() const;
|
||||
|
||||
[[nodiscard]] bool empty() const {
|
||||
return _empty.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<bool> emptyValue() const {
|
||||
return _empty.value();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<uint64> clicks() const;
|
||||
[[nodiscard]] rpl::producer<ShowMenuRequest> showMenuRequests() const;
|
||||
[[nodiscard]] rpl::producer<bool> toggleExpandedRequests() const;
|
||||
//[[nodiscard]] rpl::producer<> entered() const;
|
||||
[[nodiscard]] rpl::producer<> loadMoreRequests() const;
|
||||
|
||||
[[nodiscard]] auto verticalScrollEvents() const
|
||||
-> rpl::producer<not_null<QWheelEvent*>>;
|
||||
|
||||
private:
|
||||
struct Layout;
|
||||
enum class State {
|
||||
Small,
|
||||
Changing,
|
||||
Full,
|
||||
};
|
||||
struct Item {
|
||||
Element element;
|
||||
QImage nameCache;
|
||||
QColor nameCacheColor;
|
||||
std::vector<Ui::OutlineSegment> segments;
|
||||
bool subscribed = false;
|
||||
};
|
||||
struct Data {
|
||||
std::vector<Item> items;
|
||||
|
||||
[[nodiscard]] bool empty() const {
|
||||
return items.empty();
|
||||
}
|
||||
};
|
||||
|
||||
void showContent(Content &&content);
|
||||
//void enterEventHook(QEnterEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void wheelEvent(QWheelEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
void contextMenuEvent(QContextMenuEvent *e) override;
|
||||
|
||||
void paint(
|
||||
QPainter &p,
|
||||
const Layout &layout,
|
||||
float64 photo,
|
||||
float64 line,
|
||||
bool layered);
|
||||
void ensureLayer();
|
||||
void validateThumbnail(not_null<Item*> item);
|
||||
void validateName(not_null<Item*> item);
|
||||
void updateScrollMax();
|
||||
void updateSelected();
|
||||
void checkDragging();
|
||||
bool finishDragging();
|
||||
void checkLoadMore();
|
||||
void requestExpanded(bool expanded);
|
||||
|
||||
void updateTooltipGeometry();
|
||||
[[nodiscard]] TextWithEntities computeTooltipText() const;
|
||||
void toggleTooltip(bool fast);
|
||||
|
||||
bool checkForFullState();
|
||||
void setState(State state);
|
||||
void updateGeometry();
|
||||
[[nodiscard]] QRect countSmallGeometry() const;
|
||||
void updateExpanding();
|
||||
void updateExpanding(int expandingHeight, int expandedHeight);
|
||||
void validateSegments(
|
||||
not_null<Item*> item,
|
||||
const QBrush &brush,
|
||||
float64 line,
|
||||
bool forUnread);
|
||||
|
||||
[[nodiscard]] Layout computeLayout();
|
||||
[[nodiscard]] Layout computeLayout(float64 expanded) const;
|
||||
|
||||
const style::DialogsStoriesList &_st;
|
||||
Content _content;
|
||||
Data _data;
|
||||
rpl::event_stream<uint64> _clicks;
|
||||
rpl::event_stream<ShowMenuRequest> _showMenuRequests;
|
||||
rpl::event_stream<bool> _toggleExpandedRequests;
|
||||
//rpl::event_stream<> _entered;
|
||||
rpl::event_stream<> _loadMoreRequests;
|
||||
rpl::event_stream<> _collapsedGeometryChanged;
|
||||
|
||||
QImage _layer;
|
||||
QPoint _positionSmall;
|
||||
style::align _alignSmall = {};
|
||||
QRect _geometryFull;
|
||||
QRect _changingGeometryFrom;
|
||||
State _state = State::Small;
|
||||
rpl::variable<bool> _empty = true;
|
||||
|
||||
QPoint _lastMousePosition;
|
||||
std::optional<QPoint> _mouseDownPosition;
|
||||
int _startDraggingLeft = 0;
|
||||
int _scrollLeft = 0;
|
||||
int _scrollLeftMax = 0;
|
||||
bool _dragging = false;
|
||||
Qt::Orientation _scrollingLock = {};
|
||||
|
||||
Ui::Animations::Simple _expandedAnimation;
|
||||
Ui::Animations::Simple _expandCatchUpAnimation;
|
||||
float64 _lastRatio = 0.;
|
||||
int _lastExpandedHeight = 0;
|
||||
bool _expandIgnored : 1 = false;
|
||||
bool _expanded : 1 = false;
|
||||
|
||||
mutable CollapsedGeometry _lastCollapsedGeometry;
|
||||
mutable float64 _lastCollapsedRatio = 0.;
|
||||
|
||||
int _selected = -1;
|
||||
int _pressed = -1;
|
||||
|
||||
rpl::event_stream<not_null<QWheelEvent*>> _verticalScrollEvents;
|
||||
|
||||
rpl::variable<TextWithEntities> _tooltipText;
|
||||
rpl::variable<bool> _tooltipNotHidden;
|
||||
Fn<void()> _tooltipHide;
|
||||
std::unique_ptr<Ui::ImportantTooltip> _tooltip;
|
||||
bool _tooltipWindowActive = false;
|
||||
|
||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||
base::has_weak_ptr _menuGuard;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Dialogs::Stories
|
||||
2973
Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp
Normal file
2973
Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp
Normal file
File diff suppressed because it is too large
Load Diff
305
Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h
Normal file
305
Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h
Normal file
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
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/timer.h"
|
||||
#include "dialogs/ui/top_peers_strip.h"
|
||||
#include "ui/controls/swipe_handler_data.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
class PeerListContent;
|
||||
|
||||
namespace Data {
|
||||
class Thread;
|
||||
} // namespace Data
|
||||
|
||||
namespace Info {
|
||||
class WrapWidget;
|
||||
} // namespace Info
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Storage {
|
||||
enum class SharedMediaType : signed char;
|
||||
} // namespace Storage
|
||||
|
||||
namespace Ui::Controls {
|
||||
struct SwipeHandlerArgs;
|
||||
} // namespace Ui::Controls
|
||||
|
||||
namespace Ui {
|
||||
class BoxContent;
|
||||
class ScrollArea;
|
||||
class ElasticScroll;
|
||||
class SettingsSlider;
|
||||
class VerticalLayout;
|
||||
template <typename Widget>
|
||||
class SlideWrap;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
class InnerWidget;
|
||||
class PostsSearch;
|
||||
class PostsSearchIntro;
|
||||
struct PostsSearchIntroState;
|
||||
enum class SearchEmptyIcon;
|
||||
|
||||
struct RecentPeersList {
|
||||
std::vector<not_null<PeerData*>> list;
|
||||
};
|
||||
|
||||
class Suggestions final : public Ui::RpWidget {
|
||||
public:
|
||||
Suggestions(
|
||||
not_null<QWidget*> parent,
|
||||
not_null<Window::SessionController*> controller,
|
||||
rpl::producer<TopPeersList> topPeers,
|
||||
RecentPeersList recentPeers);
|
||||
~Suggestions();
|
||||
|
||||
void selectJump(Qt::Key direction, int pageSize = 0);
|
||||
void chooseRow();
|
||||
|
||||
bool consumeSearchQuery(const QString &query);
|
||||
[[nodiscard]] rpl::producer<> clearSearchQueryRequests() const;
|
||||
|
||||
[[nodiscard]] Data::Thread *updateFromParentDrag(QPoint globalPosition);
|
||||
void dragLeft();
|
||||
|
||||
void show(anim::type animated, Fn<void()> finish);
|
||||
void hide(anim::type animated, Fn<void()> finish);
|
||||
[[nodiscard]] float64 shownOpacity() const;
|
||||
|
||||
[[nodiscard]] bool persist() const;
|
||||
void clearPersistance();
|
||||
|
||||
[[nodiscard]] rpl::producer<not_null<PeerData*>> topPeerChosen() const {
|
||||
return _topPeerChosen.events();
|
||||
}
|
||||
[[nodiscard]] auto recentPeerChosen() const
|
||||
-> rpl::producer<not_null<PeerData*>> {
|
||||
return _recent->chosen.events();
|
||||
}
|
||||
[[nodiscard]] auto myChannelChosen() const
|
||||
-> rpl::producer<not_null<PeerData*>> {
|
||||
return _myChannels->chosen.events();
|
||||
}
|
||||
[[nodiscard]] auto recommendationChosen() const
|
||||
-> rpl::producer<not_null<PeerData*>> {
|
||||
return _recommendations->chosen.events();
|
||||
}
|
||||
[[nodiscard]] auto recentAppChosen() const
|
||||
-> rpl::producer<not_null<PeerData*>> {
|
||||
return _recentApps->chosen.events();
|
||||
}
|
||||
[[nodiscard]] auto popularAppChosen() const
|
||||
-> rpl::producer<not_null<PeerData*>> {
|
||||
return _popularApps->chosen.events();
|
||||
}
|
||||
[[nodiscard]] auto openBotMainAppRequests() const
|
||||
-> rpl::producer<not_null<PeerData*>> {
|
||||
return _openBotMainAppRequests.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<> closeRequests() const {
|
||||
return _closeRequests.events();
|
||||
}
|
||||
|
||||
class ObjectListController;
|
||||
|
||||
private:
|
||||
using MediaType = Storage::SharedMediaType;
|
||||
enum class Tab : uchar {
|
||||
Chats,
|
||||
Channels,
|
||||
Apps,
|
||||
Posts,
|
||||
Media,
|
||||
Downloads,
|
||||
};
|
||||
enum class JumpResult : uchar {
|
||||
NotApplied,
|
||||
Applied,
|
||||
AppliedAndOut,
|
||||
};
|
||||
|
||||
struct Key {
|
||||
Tab tab = Tab::Chats;
|
||||
MediaType mediaType = {};
|
||||
|
||||
friend inline auto operator<=>(Key, Key) = default;
|
||||
friend inline bool operator==(Key, Key) = default;
|
||||
};
|
||||
|
||||
struct ObjectList {
|
||||
not_null<Ui::SlideWrap<PeerListContent>*> wrap;
|
||||
rpl::variable<int> count;
|
||||
Fn<bool()> choose;
|
||||
Fn<JumpResult(Qt::Key, int)> selectJump;
|
||||
Fn<uint64(QPoint)> updateFromParentDrag;
|
||||
Fn<void()> dragLeft;
|
||||
Fn<bool(not_null<QTouchEvent*>)> processTouch;
|
||||
rpl::event_stream<not_null<PeerData*>> chosen;
|
||||
};
|
||||
|
||||
struct MediaList {
|
||||
Info::WrapWidget *wrap = nullptr;
|
||||
rpl::variable<int> count;
|
||||
};
|
||||
|
||||
[[nodiscard]] static std::vector<Key> TabKeysFor(
|
||||
not_null<Window::SessionController*> controller);
|
||||
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
void setupTabs();
|
||||
void setupChats();
|
||||
void setupChannels();
|
||||
void setupApps();
|
||||
void reinstallSwipe(not_null<Ui::ElasticScroll*>);
|
||||
[[nodiscard]] auto generateIncompleteSwipeArgs()
|
||||
-> Ui::Controls::SwipeHandlerArgs;
|
||||
|
||||
void selectJumpChats(Qt::Key direction, int pageSize);
|
||||
void selectJumpChannels(Qt::Key direction, int pageSize);
|
||||
void selectJumpApps(Qt::Key direction, int pageSize);
|
||||
|
||||
[[nodiscard]] Data::Thread *updateFromChatsDrag(QPoint globalPosition);
|
||||
[[nodiscard]] Data::Thread *updateFromChannelsDrag(
|
||||
QPoint globalPosition);
|
||||
[[nodiscard]] Data::Thread *updateFromAppsDrag(QPoint globalPosition);
|
||||
[[nodiscard]] Data::Thread *fromListId(uint64 peerListRowId);
|
||||
|
||||
[[nodiscard]] std::unique_ptr<ObjectList> setupRecentPeers(
|
||||
RecentPeersList recentPeers);
|
||||
[[nodiscard]] auto setupEmptyRecent()
|
||||
-> object_ptr<Ui::SlideWrap<Ui::RpWidget>>;
|
||||
|
||||
[[nodiscard]] std::unique_ptr<ObjectList> setupMyChannels();
|
||||
[[nodiscard]] std::unique_ptr<ObjectList> setupRecommendations();
|
||||
[[nodiscard]] auto setupEmptyChannels()
|
||||
-> object_ptr<Ui::SlideWrap<Ui::RpWidget>>;
|
||||
|
||||
[[nodiscard]] std::unique_ptr<ObjectList> setupRecentApps();
|
||||
[[nodiscard]] std::unique_ptr<ObjectList> setupPopularApps();
|
||||
|
||||
[[nodiscard]] std::unique_ptr<ObjectList> setupObjectList(
|
||||
not_null<Ui::ElasticScroll*> scroll,
|
||||
not_null<Ui::VerticalLayout*> parent,
|
||||
not_null<ObjectListController*> controller,
|
||||
Fn<int()> addToScroll = nullptr);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::SlideWrap<Ui::RpWidget>> setupEmpty(
|
||||
not_null<QWidget*> parent,
|
||||
SearchEmptyIcon icon,
|
||||
rpl::producer<QString> text);
|
||||
|
||||
void switchTab(Key key);
|
||||
void startShownAnimation(bool shown, Fn<void()> finish);
|
||||
void startSlideAnimation(Key was, Key now);
|
||||
void ensureContent(Key key);
|
||||
void finishShow();
|
||||
|
||||
void handlePressForChatPreview(PeerId id, Fn<void(bool)> callback);
|
||||
void updateControlsGeometry();
|
||||
void applySearchQuery();
|
||||
|
||||
void setupPostsSearch();
|
||||
void setPostsSearchQuery(const QString &query);
|
||||
void setupPostsResults();
|
||||
void setupPostsIntro(const PostsSearchIntroState &intro);
|
||||
void updatePostsSearchVisibleRange();
|
||||
|
||||
const not_null<Window::SessionController*> _controller;
|
||||
|
||||
const std::unique_ptr<Ui::ScrollArea> _tabsScroll;
|
||||
const not_null<Ui::SettingsSlider*> _tabs;
|
||||
Ui::Animations::Simple _tabsScrollAnimation;
|
||||
const std::vector<Key> _tabKeys;
|
||||
rpl::variable<Key> _key;
|
||||
|
||||
const std::unique_ptr<Ui::ElasticScroll> _chatsScroll;
|
||||
const not_null<Ui::VerticalLayout*> _chatsContent;
|
||||
|
||||
const not_null<Ui::SlideWrap<TopPeersStrip>*> _topPeersWrap;
|
||||
const not_null<TopPeersStrip*> _topPeers;
|
||||
rpl::event_stream<not_null<PeerData*>> _topPeerChosen;
|
||||
rpl::event_stream<not_null<PeerData*>> _openBotMainAppRequests;
|
||||
rpl::event_stream<> _closeRequests;
|
||||
|
||||
const std::unique_ptr<ObjectList> _recent;
|
||||
|
||||
const not_null<Ui::SlideWrap<Ui::RpWidget>*> _emptyRecent;
|
||||
|
||||
const std::unique_ptr<Ui::ElasticScroll> _channelsScroll;
|
||||
const not_null<Ui::VerticalLayout*> _channelsContent;
|
||||
|
||||
const std::unique_ptr<ObjectList> _myChannels;
|
||||
const std::unique_ptr<ObjectList> _recommendations;
|
||||
|
||||
const not_null<Ui::SlideWrap<Ui::RpWidget>*> _emptyChannels;
|
||||
|
||||
const std::unique_ptr<Ui::ElasticScroll> _appsScroll;
|
||||
const not_null<Ui::VerticalLayout*> _appsContent;
|
||||
|
||||
std::unique_ptr<PostsSearch> _postsSearch;
|
||||
const std::unique_ptr<Ui::ElasticScroll> _postsScroll;
|
||||
const not_null<Ui::RpWidget*> _postsWrap;
|
||||
PostsSearchIntro *_postsSearchIntro = nullptr;
|
||||
InnerWidget *_postsContent = nullptr;
|
||||
|
||||
rpl::producer<> _recentAppsRefreshed;
|
||||
Fn<bool(not_null<PeerData*>)> _recentAppsShows;
|
||||
const std::unique_ptr<ObjectList> _recentApps;
|
||||
const std::unique_ptr<ObjectList> _popularApps;
|
||||
|
||||
base::flat_map<Key, MediaList> _mediaLists;
|
||||
rpl::event_stream<> _clearSearchQueryRequests;
|
||||
QString _searchQuery;
|
||||
base::Timer _searchQueryTimer;
|
||||
|
||||
Ui::Animations::Simple _shownAnimation;
|
||||
Fn<void()> _showFinished;
|
||||
bool _hidden = false;
|
||||
bool _persist = false;
|
||||
QPixmap _cache;
|
||||
|
||||
Ui::Animations::Simple _slideAnimation;
|
||||
QPixmap _slideLeft;
|
||||
QPixmap _slideRight;
|
||||
|
||||
Ui::Controls::SwipeBackResult _swipeBackData;
|
||||
rpl::lifetime _swipeLifetime;
|
||||
|
||||
int _slideLeftTop = 0;
|
||||
int _slideRightTop = 0;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] rpl::producer<TopPeersList> TopPeersContent(
|
||||
not_null<Main::Session*> session);
|
||||
|
||||
[[nodiscard]] RecentPeersList RecentPeersContent(
|
||||
not_null<Main::Session*> session);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> StarsExamplesBox(
|
||||
not_null<Window::SessionController*> window);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::BoxContent> PopularAppsAboutBox(
|
||||
not_null<Window::SessionController*> window);
|
||||
|
||||
} // namespace Dialogs
|
||||
@@ -0,0 +1,527 @@
|
||||
/*
|
||||
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 "dialogs/ui/dialogs_top_bar_suggestion_content.h"
|
||||
|
||||
#include "base/call_delayed.h"
|
||||
#include "data/data_authorization.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "lottie/lottie_icon.h"
|
||||
#include "settings/settings_common.h"
|
||||
#include "ui/effects/animation_value.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/power_saving.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/text/text_custom_emoji.h"
|
||||
#include "ui/ui_rpl_filter.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/shadow.h"
|
||||
#include "ui/wrap/fade_wrap.h"
|
||||
#include "ui/wrap/padding_wrap.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_premium.h"
|
||||
#include "styles/style_settings.h"
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
class UnconfirmedAuthWrap : public Ui::SlideWrap<Ui::VerticalLayout> {
|
||||
public:
|
||||
UnconfirmedAuthWrap(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
object_ptr<Ui::VerticalLayout> &&child)
|
||||
: Ui::SlideWrap<Ui::VerticalLayout>(parent, std::move(child)) {
|
||||
}
|
||||
|
||||
rpl::producer<int> desiredHeightValue() const override {
|
||||
return entity()->heightValue();
|
||||
}
|
||||
};
|
||||
|
||||
not_null<Ui::SlideWrap<Ui::VerticalLayout>*> CreateUnconfirmedAuthContent(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const std::vector<Data::UnreviewedAuth> &list,
|
||||
Fn<void(bool)> callback) {
|
||||
const auto wrap = Ui::CreateChild<UnconfirmedAuthWrap>(
|
||||
parent,
|
||||
object_ptr<Ui::VerticalLayout>(parent));
|
||||
const auto content = wrap->entity();
|
||||
content->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(content);
|
||||
p.fillRect(content->rect(), st::dialogsBg);
|
||||
}, content->lifetime());
|
||||
|
||||
const auto padding = st::dialogsUnconfirmedAuthPadding;
|
||||
|
||||
Ui::AddSkip(content);
|
||||
|
||||
content->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
content,
|
||||
tr::lng_unconfirmed_auth_title(),
|
||||
st::dialogsUnconfirmedAuthTitle),
|
||||
padding,
|
||||
style::al_top);
|
||||
|
||||
Ui::AddSkip(content);
|
||||
|
||||
auto messageText = QString();
|
||||
if (list.size() == 1) {
|
||||
const auto &auth = list.at(0);
|
||||
messageText = tr::lng_unconfirmed_auth_single(
|
||||
tr::now,
|
||||
lt_from,
|
||||
auth.device,
|
||||
lt_country,
|
||||
auth.location);
|
||||
} else {
|
||||
auto commonLocation = list.at(0).location;
|
||||
for (auto i = 1; i < list.size(); ++i) {
|
||||
if (commonLocation != list.at(i).location) {
|
||||
commonLocation.clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (commonLocation.isEmpty()) {
|
||||
messageText = tr::lng_unconfirmed_auth_multiple(
|
||||
tr::now,
|
||||
lt_count,
|
||||
list.size());
|
||||
} else {
|
||||
messageText = tr::lng_unconfirmed_auth_multiple_from(
|
||||
tr::now,
|
||||
lt_count,
|
||||
list.size(),
|
||||
lt_country,
|
||||
commonLocation);
|
||||
}
|
||||
}
|
||||
|
||||
content->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
content,
|
||||
rpl::single(messageText),
|
||||
st::dialogsUnconfirmedAuthAbout),
|
||||
padding,
|
||||
style::al_top)->setTryMakeSimilarLines(true);
|
||||
|
||||
Ui::AddSkip(content);
|
||||
const auto buttons = content->add(object_ptr<Ui::FixedHeightWidget>(
|
||||
content,
|
||||
st::dialogsUnconfirmedAuthButton.height));
|
||||
const auto yes = Ui::CreateChild<Ui::RoundButton>(
|
||||
buttons,
|
||||
tr::lng_unconfirmed_auth_confirm(),
|
||||
st::dialogsUnconfirmedAuthButton);
|
||||
const auto no = Ui::CreateChild<Ui::RoundButton>(
|
||||
buttons,
|
||||
tr::lng_unconfirmed_auth_deny(),
|
||||
st::dialogsUnconfirmedAuthButtonNo);
|
||||
yes->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
|
||||
no->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
|
||||
yes->setClickedCallback([=] {
|
||||
wrap->toggle(false, anim::type::normal);
|
||||
base::call_delayed(st::universalDuration, wrap, [=] {
|
||||
callback(true);
|
||||
});
|
||||
});
|
||||
no->setClickedCallback([=] {
|
||||
wrap->toggle(false, anim::type::normal);
|
||||
base::call_delayed(st::universalDuration, wrap, [=] {
|
||||
callback(false);
|
||||
});
|
||||
});
|
||||
buttons->sizeValue(
|
||||
) | rpl::filter_size(
|
||||
) | rpl::on_next([=](const QSize &s) {
|
||||
const auto halfWidth = (s.width() - rect::m::sum::h(padding)) / 2;
|
||||
yes->moveToLeft(
|
||||
padding.left() + (halfWidth - yes->width()) / 2,
|
||||
0);
|
||||
no->moveToLeft(
|
||||
padding.left() + halfWidth + (halfWidth - no->width()) / 2,
|
||||
0);
|
||||
}, buttons->lifetime());
|
||||
Ui::AddSkip(content);
|
||||
content->add(object_ptr<Ui::FadeShadow>(content));
|
||||
|
||||
return wrap;
|
||||
}
|
||||
|
||||
void ShowAuthDeniedBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
float64 count,
|
||||
const QString &messageText) {
|
||||
box->setStyle(st::showOrBox);
|
||||
box->setWidth(st::boxWideWidth);
|
||||
const auto buttonPadding = QMargins(
|
||||
st::showOrBox.buttonPadding.left(),
|
||||
0,
|
||||
st::showOrBox.buttonPadding.right(),
|
||||
0);
|
||||
auto icon = Settings::CreateLottieIcon(
|
||||
box,
|
||||
{
|
||||
.name = u"ban"_q,
|
||||
.sizeOverride = st::dialogsSuggestionDeniedAuthLottie,
|
||||
},
|
||||
st::dialogsSuggestionDeniedAuthLottieMargins);
|
||||
Settings::AddLottieIconWithCircle(
|
||||
box->verticalLayout(),
|
||||
std::move(icon.widget),
|
||||
st::settingsBlockedListIconPadding,
|
||||
st::dialogsSuggestionDeniedAuthLottieCircle);
|
||||
box->setShowFinishedCallback([=, animate = std::move(icon.animate)] {
|
||||
animate(anim::repeat::once);
|
||||
});
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
tr::lng_unconfirmed_auth_denied_title(
|
||||
lt_count,
|
||||
rpl::single(count)),
|
||||
st::boostCenteredTitle),
|
||||
st::showOrTitlePadding + buttonPadding,
|
||||
style::al_top);
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
messageText,
|
||||
st::boostText),
|
||||
st::showOrAboutPadding + buttonPadding,
|
||||
style::al_top);
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
const auto warning = box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
tr::lng_unconfirmed_auth_denied_warning(tr::bold),
|
||||
st::boostText),
|
||||
st::showOrAboutPadding + buttonPadding
|
||||
+ QMargins(st::boostTextSkip, 0, st::boostTextSkip, 0),
|
||||
style::al_top);
|
||||
warning->setTextColorOverride(st::attentionButtonFg->c);
|
||||
const auto warningBg = Ui::CreateChild<Ui::RpWidget>(
|
||||
box->verticalLayout());
|
||||
warning->geometryValue() | rpl::on_next([=](QRect r) {
|
||||
warningBg->setGeometry(r + Margins(st::boostTextSkip));
|
||||
}, warningBg->lifetime());
|
||||
warningBg->paintOn([=](QPainter &p) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::attentionButtonBgOver);
|
||||
p.drawRoundedRect(
|
||||
warningBg->rect(),
|
||||
st::buttonRadius,
|
||||
st::buttonRadius);
|
||||
});
|
||||
warningBg->show();
|
||||
warning->raise();
|
||||
warningBg->stackUnder(warning);
|
||||
const auto confirm = box->addButton(
|
||||
object_ptr<Ui::RoundButton>(
|
||||
box,
|
||||
rpl::single(QString()),
|
||||
st::defaultActiveButton));
|
||||
confirm->setClickedCallback([=] {
|
||||
box->closeBox();
|
||||
});
|
||||
confirm->resize(
|
||||
st::showOrShowButton.width,
|
||||
st::showOrShowButton.height);
|
||||
|
||||
const auto textLabel = Ui::CreateChild<Ui::FlatLabel>(
|
||||
confirm,
|
||||
tr::lng_archive_hint_button(),
|
||||
st::defaultSubsectionTitle);
|
||||
textLabel->setTextColorOverride(st::defaultActiveButton.textFg->c);
|
||||
textLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
const auto timerLabel = Ui::CreateChild<Ui::FlatLabel>(
|
||||
confirm,
|
||||
rpl::single(QString()),
|
||||
st::defaultSubsectionTitle);
|
||||
timerLabel->setTextColorOverride(
|
||||
anim::with_alpha(st::defaultActiveButton.textFg->c, 0.75));
|
||||
timerLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
constexpr auto kTimer = 5;
|
||||
const auto remaining = confirm->lifetime().make_state<int>(kTimer);
|
||||
const auto timerLifetime
|
||||
= confirm->lifetime().make_state<rpl::lifetime>();
|
||||
const auto timer = timerLifetime->make_state<base::Timer>([=] {
|
||||
if ((*remaining) > 0) {
|
||||
timerLabel->setText(QString::number((*remaining)--));
|
||||
} else {
|
||||
timerLabel->hide();
|
||||
confirm->setAttribute(Qt::WA_TransparentForMouseEvents, false);
|
||||
box->setCloseByEscape(true);
|
||||
box->setCloseByOutsideClick(true);
|
||||
timerLifetime->destroy();
|
||||
}
|
||||
});
|
||||
box->setCloseByEscape(false);
|
||||
box->setCloseByOutsideClick(false);
|
||||
confirm->setAttribute(Qt::WA_TransparentForMouseEvents, true);
|
||||
timerLabel->setText(QString::number((*remaining)));
|
||||
timer->callEach(1000);
|
||||
|
||||
rpl::combine(
|
||||
confirm->sizeValue(),
|
||||
textLabel->sizeValue(),
|
||||
timerLabel->sizeValue(),
|
||||
timerLabel->shownValue()
|
||||
) | rpl::on_next([=](QSize btn, QSize text, QSize timer, bool shown) {
|
||||
const auto skip = st::normalFont->spacew;
|
||||
const auto totalWidth = shown
|
||||
? (text.width() + skip + timer.width())
|
||||
: text.width();
|
||||
const auto left = (btn.width() - totalWidth) / 2;
|
||||
textLabel->moveToLeft(left, (btn.height() - text.height()) / 2);
|
||||
timerLabel->moveToLeft(
|
||||
left + text.width() + skip,
|
||||
(btn.height() - timer.height()) / 2);
|
||||
}, confirm->lifetime());
|
||||
}
|
||||
|
||||
TopBarSuggestionContent::TopBarSuggestionContent(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
Fn<bool()> emojiPaused)
|
||||
: Ui::RippleButton(parent, st::defaultRippleAnimationBgOver)
|
||||
, _titleSt(st::semiboldTextStyle)
|
||||
, _contentTitleSt(st::dialogsTopBarSuggestionTitleStyle)
|
||||
, _contentTextSt(st::dialogsTopBarSuggestionAboutStyle)
|
||||
, _emojiPaused(std::move(emojiPaused)) {
|
||||
setRightIcon(RightIcon::Close);
|
||||
}
|
||||
|
||||
void TopBarSuggestionContent::setRightIcon(RightIcon icon) {
|
||||
_rightButton = nullptr;
|
||||
if (icon == _rightIcon) {
|
||||
return;
|
||||
}
|
||||
_rightHide = nullptr;
|
||||
_rightArrow = nullptr;
|
||||
_rightIcon = icon;
|
||||
if (icon == RightIcon::Close) {
|
||||
_rightHide = base::make_unique_q<Ui::IconButton>(
|
||||
this,
|
||||
st::dialogsCancelSearchInPeer);
|
||||
const auto rightHide = _rightHide.get();
|
||||
sizeValue() | rpl::filter_size(
|
||||
) | rpl::on_next([=](const QSize &s) {
|
||||
rightHide->moveToRight(st::buttonRadius, st::lineWidth);
|
||||
}, rightHide->lifetime());
|
||||
rightHide->show();
|
||||
} else if (icon == RightIcon::Arrow) {
|
||||
_rightArrow = base::make_unique_q<Ui::IconButton>(
|
||||
this,
|
||||
st::backButton);
|
||||
const auto arrow = _rightArrow.get();
|
||||
arrow->setIconOverride(
|
||||
&st::settingsPremiumArrow,
|
||||
&st::settingsPremiumArrowOver);
|
||||
arrow->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
sizeValue() | rpl::filter_size(
|
||||
) | rpl::on_next([=](const QSize &s) {
|
||||
const auto &point = st::settingsPremiumArrowShift;
|
||||
arrow->moveToLeft(
|
||||
s.width() - arrow->width(),
|
||||
point.y() + (s.height() - arrow->height()) / 2);
|
||||
}, arrow->lifetime());
|
||||
arrow->show();
|
||||
}
|
||||
}
|
||||
|
||||
void TopBarSuggestionContent::setRightButton(
|
||||
rpl::producer<TextWithEntities> text,
|
||||
Fn<void()> callback) {
|
||||
_rightHide = nullptr;
|
||||
_rightArrow = nullptr;
|
||||
_rightIcon = RightIcon::None;
|
||||
if (!text) {
|
||||
_rightButton = nullptr;
|
||||
return;
|
||||
}
|
||||
using namespace Ui;
|
||||
_rightButton = base::make_unique_q<RoundButton>(
|
||||
this,
|
||||
rpl::single(QString()),
|
||||
st::dialogsTopBarRightButton);
|
||||
_rightButton->setText(std::move(text));
|
||||
rpl::combine(
|
||||
sizeValue(),
|
||||
_rightButton->sizeValue()
|
||||
) | rpl::on_next([=](QSize outer, QSize inner) {
|
||||
const auto top = (outer.height() - inner.height()) / 2;
|
||||
_rightButton->moveToRight(top, top, outer.width());
|
||||
}, _rightButton->lifetime());
|
||||
_rightButton->setFullRadius(true);
|
||||
_rightButton->setTextTransform(RoundButton::TextTransform::NoTransform);
|
||||
_rightButton->setClickedCallback(std::move(callback));
|
||||
_rightButton->show();
|
||||
}
|
||||
|
||||
void TopBarSuggestionContent::draw(QPainter &p) {
|
||||
const auto kLinesForPhoto = 3;
|
||||
|
||||
const auto r = Ui::RpWidget::rect();
|
||||
p.fillRect(r, st::historyPinnedBg);
|
||||
p.fillRect(
|
||||
r.x(),
|
||||
r.y() + r.height() - st::lineWidth,
|
||||
r.width(),
|
||||
st::lineWidth,
|
||||
st::shadowFg);
|
||||
Ui::RippleButton::paintRipple(p, 0, 0);
|
||||
const auto leftPadding = _leftPadding;
|
||||
const auto rightPadding = 0;
|
||||
const auto topPadding = st::msgReplyPadding.top();
|
||||
const auto availableWidthNoPhoto = r.width()
|
||||
- (_rightArrow
|
||||
? (_rightArrow->width() / 4 * 3) // Takes full height.
|
||||
: 0)
|
||||
- leftPadding
|
||||
- rightPadding;
|
||||
const auto availableWidth = availableWidthNoPhoto
|
||||
- (_rightHide ? _rightHide->width() : 0);
|
||||
const auto titleRight = leftPadding;
|
||||
const auto hasSecondLineTitle = availableWidth < _contentTitle.maxWidth();
|
||||
const auto paused = On(PowerSaving::kEmojiChat)
|
||||
|| (_emojiPaused && _emojiPaused());
|
||||
p.setPen(st::windowActiveTextFg);
|
||||
p.setPen(st::windowFg);
|
||||
{
|
||||
const auto left = leftPadding;
|
||||
const auto top = topPadding;
|
||||
_contentTitle.draw(p, {
|
||||
.position = QPoint(left, top),
|
||||
.outerWidth = hasSecondLineTitle
|
||||
? availableWidth
|
||||
: (availableWidth - titleRight),
|
||||
.availableWidth = availableWidth,
|
||||
.pausedEmoji = paused,
|
||||
.elisionLines = hasSecondLineTitle ? 2 : 1,
|
||||
});
|
||||
}
|
||||
{
|
||||
const auto left = leftPadding;
|
||||
const auto top = hasSecondLineTitle
|
||||
? (topPadding
|
||||
+ _titleSt.font->height
|
||||
+ _contentTitleSt.font->height)
|
||||
: topPadding + _titleSt.font->height;
|
||||
auto lastContentLineAmount = 0;
|
||||
const auto lineHeight = _contentTextSt.font->height;
|
||||
const auto lineLayout = [&](int line) -> Ui::Text::LineGeometry {
|
||||
line++;
|
||||
lastContentLineAmount = line;
|
||||
const auto diff = (st::sponsoredMessageBarMaxHeight)
|
||||
- line * lineHeight;
|
||||
if (diff < 3 * lineHeight) {
|
||||
return {
|
||||
.width = availableWidthNoPhoto,
|
||||
.elided = true,
|
||||
};
|
||||
} else if (diff < 2 * lineHeight) {
|
||||
return {};
|
||||
}
|
||||
line += (hasSecondLineTitle ? 2 : 1) + 1;
|
||||
return {
|
||||
.width = (line > kLinesForPhoto)
|
||||
? availableWidthNoPhoto
|
||||
: availableWidth,
|
||||
};
|
||||
};
|
||||
p.setPen(_descriptionColorOverride.value_or(st::windowSubTextFg->c));
|
||||
_contentText.draw(p, {
|
||||
.position = QPoint(left, top),
|
||||
.outerWidth = availableWidth,
|
||||
.availableWidth = availableWidth,
|
||||
.geometry = Ui::Text::GeometryDescriptor{
|
||||
.layout = std::move(lineLayout),
|
||||
},
|
||||
.pausedEmoji = paused,
|
||||
});
|
||||
_lastPaintedContentTop = top;
|
||||
_lastPaintedContentLineAmount = lastContentLineAmount;
|
||||
}
|
||||
}
|
||||
|
||||
void TopBarSuggestionContent::setContent(
|
||||
TextWithEntities title,
|
||||
TextWithEntities description,
|
||||
std::optional<Ui::Text::MarkedContext> context,
|
||||
std::optional<QColor> descriptionColorOverride) {
|
||||
_descriptionColorOverride = descriptionColorOverride;
|
||||
if (context) {
|
||||
context->repaint = [=] { update(); };
|
||||
_contentTitle.setMarkedText(
|
||||
_contentTitleSt,
|
||||
std::move(title),
|
||||
kMarkupTextOptions,
|
||||
*context);
|
||||
_contentText.setMarkedText(
|
||||
_contentTextSt,
|
||||
std::move(description),
|
||||
kMarkupTextOptions,
|
||||
base::take(*context));
|
||||
} else {
|
||||
_contentTitle.setMarkedText(_contentTitleSt, std::move(title));
|
||||
_contentText.setMarkedText(_contentTextSt, std::move(description));
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void TopBarSuggestionContent::paintEvent(QPaintEvent *) {
|
||||
auto p = QPainter(this);
|
||||
draw(p);
|
||||
}
|
||||
|
||||
rpl::producer<int> TopBarSuggestionContent::desiredHeightValue() const {
|
||||
return rpl::combine(
|
||||
_lastPaintedContentTop.value(),
|
||||
_lastPaintedContentLineAmount.value()
|
||||
) | rpl::distinct_until_changed() | rpl::map([=](
|
||||
int lastTop,
|
||||
int lastLines) {
|
||||
const auto bottomPadding = st::msgReplyPadding.top();
|
||||
const auto desiredHeight = lastTop
|
||||
+ (lastLines * _contentTextSt.font->height)
|
||||
+ bottomPadding;
|
||||
return std::min(desiredHeight, st::sponsoredMessageBarMaxHeight);
|
||||
});
|
||||
}
|
||||
|
||||
void TopBarSuggestionContent::setHideCallback(Fn<void()> hideCallback) {
|
||||
Expects(_rightHide != nullptr);
|
||||
_rightHide->setClickedCallback(std::move(hideCallback));
|
||||
}
|
||||
|
||||
void TopBarSuggestionContent::setLeftPadding(rpl::producer<int> value) {
|
||||
std::move(value) | rpl::on_next([=](int padding) {
|
||||
_leftPadding = padding;
|
||||
update();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
const style::TextStyle & TopBarSuggestionContent::contentTitleSt() const {
|
||||
return _contentTitleSt;
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
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/buttons.h"
|
||||
|
||||
namespace Ui {
|
||||
class DynamicImage;
|
||||
class GenericBox;
|
||||
class IconButton;
|
||||
class VerticalLayout;
|
||||
template<typename Widget>
|
||||
class SlideWrap;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui::Text {
|
||||
struct MarkedContext;
|
||||
} // namespace Ui::Text
|
||||
|
||||
namespace Data {
|
||||
struct UnreviewedAuth;
|
||||
} // namespace Data
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
not_null<Ui::SlideWrap<Ui::VerticalLayout>*> CreateUnconfirmedAuthContent(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const std::vector<Data::UnreviewedAuth> &list,
|
||||
Fn<void(bool)> callback);
|
||||
|
||||
void ShowAuthDeniedBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
float64 count,
|
||||
const QString &messageText);
|
||||
|
||||
class TopBarSuggestionContent : public Ui::RippleButton {
|
||||
public:
|
||||
enum class RightIcon {
|
||||
None,
|
||||
Close,
|
||||
Arrow,
|
||||
};
|
||||
|
||||
TopBarSuggestionContent(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
Fn<bool()> emojiPaused = nullptr);
|
||||
|
||||
void setContent(
|
||||
TextWithEntities title,
|
||||
TextWithEntities description,
|
||||
std::optional<Ui::Text::MarkedContext> context = std::nullopt,
|
||||
std::optional<QColor> descriptionColorOverride = std::nullopt);
|
||||
|
||||
[[nodiscard]] rpl::producer<int> desiredHeightValue() const override;
|
||||
|
||||
void setHideCallback(Fn<void()>);
|
||||
void setRightIcon(RightIcon);
|
||||
void setRightButton(
|
||||
rpl::producer<TextWithEntities> text,
|
||||
Fn<void()> callback);
|
||||
void setLeftPadding(rpl::producer<int>);
|
||||
|
||||
[[nodiscard]] const style::TextStyle &contentTitleSt() const;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override;
|
||||
|
||||
private:
|
||||
void draw(QPainter &p);
|
||||
|
||||
const style::TextStyle &_titleSt;
|
||||
const style::TextStyle &_contentTitleSt;
|
||||
const style::TextStyle &_contentTextSt;
|
||||
|
||||
Ui::Text::String _contentTitle;
|
||||
Ui::Text::String _contentText;
|
||||
rpl::variable<int> _lastPaintedContentLineAmount = 0;
|
||||
rpl::variable<int> _lastPaintedContentTop = 0;
|
||||
std::optional<QColor> _descriptionColorOverride;
|
||||
|
||||
base::unique_qptr<Ui::IconButton> _rightHide;
|
||||
base::unique_qptr<Ui::IconButton> _rightArrow;
|
||||
base::unique_qptr<Ui::RoundButton> _rightButton;
|
||||
Fn<void()> _hideCallback;
|
||||
Fn<bool()> _emojiPaused;
|
||||
|
||||
int _leftPadding = 0;
|
||||
|
||||
RightIcon _rightIcon = RightIcon::None;
|
||||
|
||||
std::shared_ptr<Ui::DynamicImage> _rightPhoto;
|
||||
QImage _rightPhotoImage;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Dialogs
|
||||
442
Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp
Normal file
442
Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp
Normal file
@@ -0,0 +1,442 @@
|
||||
/*
|
||||
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 "dialogs/ui/dialogs_topics_view.h"
|
||||
|
||||
#include "dialogs/ui/dialogs_layout.h"
|
||||
#include "data/stickers/data_custom_emoji.h"
|
||||
#include "data/data_forum.h"
|
||||
#include "data/data_forum_topic.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_saved_messages.h"
|
||||
#include "data/data_saved_sublist.h"
|
||||
#include "data/data_session.h"
|
||||
#include "core/ui_integration.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/power_saving.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
|
||||
namespace Dialogs::Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kIconLoopCount = 1;
|
||||
|
||||
} // namespace
|
||||
|
||||
TopicsView::TopicsView(Data::Forum *forum, Data::SavedMessages *monoforum)
|
||||
: _forum(forum)
|
||||
, _monoforum(monoforum) {
|
||||
}
|
||||
|
||||
TopicsView::~TopicsView() = default;
|
||||
|
||||
bool TopicsView::prepared() const {
|
||||
const auto version = _forum
|
||||
? _forum->recentTopicsListVersion()
|
||||
: _monoforum->recentSublistsListVersion();
|
||||
return (_version == version);
|
||||
}
|
||||
|
||||
void TopicsView::prepare(MsgId frontRootId, Fn<void()> customEmojiRepaint) {
|
||||
Expects(_forum != nullptr);
|
||||
|
||||
const auto &list = _forum->recentTopics();
|
||||
_version = _forum->recentTopicsListVersion();
|
||||
_titles.reserve(list.size());
|
||||
auto index = 0;
|
||||
for (const auto &topic : list) {
|
||||
const auto from = begin(_titles) + index;
|
||||
const auto key = topic->rootId().bare;
|
||||
const auto i = ranges::find(
|
||||
from,
|
||||
end(_titles),
|
||||
key,
|
||||
&Title::key);
|
||||
if (i != end(_titles)) {
|
||||
if (i != from) {
|
||||
ranges::rotate(from, i, i + 1);
|
||||
}
|
||||
} else if (index >= _titles.size()) {
|
||||
_titles.emplace_back();
|
||||
}
|
||||
auto &title = _titles[index++];
|
||||
const auto unread = topic->chatListBadgesState().unread;
|
||||
if (title.key == key
|
||||
&& title.unread == unread
|
||||
&& title.version == topic->titleVersion()) {
|
||||
continue;
|
||||
}
|
||||
const auto context = Core::TextContext({
|
||||
.session = &topic->session(),
|
||||
.repaint = customEmojiRepaint,
|
||||
.customEmojiLoopLimit = kIconLoopCount,
|
||||
});
|
||||
auto topicTitle = topic->titleWithIcon();
|
||||
title.key = key;
|
||||
title.version = topic->titleVersion();
|
||||
title.unread = unread;
|
||||
title.title.setMarkedText(
|
||||
st::dialogsTextStyle,
|
||||
(unread
|
||||
? Ui::Text::Colorized(
|
||||
Ui::Text::Wrapped(
|
||||
std::move(topicTitle),
|
||||
EntityType::Bold))
|
||||
: std::move(topicTitle)),
|
||||
DialogTextOptions(),
|
||||
context);
|
||||
}
|
||||
while (_titles.size() > index) {
|
||||
_titles.pop_back();
|
||||
}
|
||||
const auto i = frontRootId
|
||||
? ranges::find(_titles, frontRootId.bare, &Title::key)
|
||||
: end(_titles);
|
||||
_jumpToTopic = (i != end(_titles));
|
||||
if (_jumpToTopic) {
|
||||
if (i != begin(_titles)) {
|
||||
ranges::rotate(begin(_titles), i, i + 1);
|
||||
}
|
||||
if (!_titles.front().unread) {
|
||||
_jumpToTopic = false;
|
||||
}
|
||||
}
|
||||
_allLoaded = _forum->topicsList()->loaded();
|
||||
}
|
||||
|
||||
void TopicsView::prepare(PeerId frontPeerId, Fn<void()> customEmojiRepaint) {
|
||||
Expects(_monoforum != nullptr);
|
||||
|
||||
const auto &list = _monoforum->recentSublists();
|
||||
const auto manager = &_monoforum->session().data().customEmojiManager();
|
||||
_version = _monoforum->recentSublistsListVersion();
|
||||
_titles.reserve(list.size());
|
||||
auto index = 0;
|
||||
for (const auto &sublist : list) {
|
||||
const auto from = begin(_titles) + index;
|
||||
const auto peer = sublist->sublistPeer();
|
||||
const auto key = peer->id.value;
|
||||
const auto i = ranges::find(
|
||||
from,
|
||||
end(_titles),
|
||||
key,
|
||||
&Title::key);
|
||||
if (i != end(_titles)) {
|
||||
if (i != from) {
|
||||
ranges::rotate(from, i, i + 1);
|
||||
}
|
||||
} else if (index >= _titles.size()) {
|
||||
_titles.emplace_back();
|
||||
}
|
||||
auto &title = _titles[index++];
|
||||
const auto unread = sublist->chatListBadgesState().unread;
|
||||
if (title.key == key
|
||||
&& title.unread == unread
|
||||
&& title.version == peer->nameVersion()) {
|
||||
continue;
|
||||
}
|
||||
const auto context = Core::TextContext({
|
||||
.session = &sublist->session(),
|
||||
.repaint = customEmojiRepaint,
|
||||
.customEmojiLoopLimit = kIconLoopCount,
|
||||
});
|
||||
auto topicTitle = TextWithEntities().append(
|
||||
Ui::Text::SingleCustomEmoji(
|
||||
manager->peerUserpicEmojiData(peer),
|
||||
u"@"_q)
|
||||
).append(' ').append(peer->shortName());
|
||||
title.key = key;
|
||||
title.version = peer->nameVersion();
|
||||
title.unread = unread;
|
||||
title.title.setMarkedText(
|
||||
st::dialogsTextStyle,
|
||||
(unread
|
||||
? Ui::Text::Colorized(
|
||||
Ui::Text::Wrapped(
|
||||
std::move(topicTitle),
|
||||
EntityType::Bold))
|
||||
: std::move(topicTitle)),
|
||||
DialogTextOptions(),
|
||||
context);
|
||||
}
|
||||
while (_titles.size() > index) {
|
||||
_titles.pop_back();
|
||||
}
|
||||
const auto i = frontPeerId
|
||||
? ranges::find(_titles, frontPeerId.value, &Title::key)
|
||||
: end(_titles);
|
||||
_jumpToTopic = (i != end(_titles));
|
||||
if (_jumpToTopic) {
|
||||
if (i != begin(_titles)) {
|
||||
ranges::rotate(begin(_titles), i, i + 1);
|
||||
}
|
||||
if (!_titles.front().unread) {
|
||||
_jumpToTopic = false;
|
||||
}
|
||||
}
|
||||
_allLoaded = _monoforum->chatsList()->loaded();
|
||||
}
|
||||
|
||||
int TopicsView::jumpToTopicWidth() const {
|
||||
return _jumpToTopic ? _titles.front().title.maxWidth() : 0;
|
||||
}
|
||||
|
||||
void TopicsView::paint(
|
||||
Painter &p,
|
||||
const QRect &geometry,
|
||||
const PaintContext &context) const {
|
||||
p.setFont(st::dialogsTextFont);
|
||||
p.setPen(context.active
|
||||
? st::dialogsTextFgActive
|
||||
: context.selected
|
||||
? st::dialogsTextFgOver
|
||||
: st::dialogsTextFg);
|
||||
const auto palette = &(context.active
|
||||
? st::dialogsTextPaletteArchiveActive
|
||||
: context.selected
|
||||
? st::dialogsTextPaletteArchiveOver
|
||||
: st::dialogsTextPaletteArchive);
|
||||
auto rect = geometry;
|
||||
rect.setWidth(rect.width() - _lastTopicJumpGeometry.rightCut);
|
||||
auto skipBig = _jumpToTopic && !context.active;
|
||||
if (_titles.empty()) {
|
||||
const auto text = (_monoforum && _allLoaded)
|
||||
? tr::lng_filters_no_chats(tr::now)
|
||||
: tr::lng_contacts_loading(tr::now);
|
||||
p.drawText(
|
||||
rect.x(),
|
||||
rect.y() + st::normalFont->ascent,
|
||||
text);
|
||||
return;
|
||||
}
|
||||
for (const auto &title : _titles) {
|
||||
if (rect.width() < title.title.style()->font->elidew) {
|
||||
break;
|
||||
}
|
||||
title.title.draw(p, {
|
||||
.position = rect.topLeft(),
|
||||
.availableWidth = rect.width(),
|
||||
.palette = palette,
|
||||
.spoiler = Text::DefaultSpoilerCache(),
|
||||
.now = context.now,
|
||||
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
|
||||
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
|
||||
.elisionLines = 1,
|
||||
});
|
||||
const auto skip = skipBig
|
||||
? context.st->topicsSkipBig
|
||||
: context.st->topicsSkip;
|
||||
rect.setLeft(rect.left() + title.title.maxWidth() + skip);
|
||||
skipBig = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool TopicsView::changeTopicJumpGeometry(JumpToLastGeometry geometry) {
|
||||
if (_lastTopicJumpGeometry != geometry) {
|
||||
_lastTopicJumpGeometry = geometry;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void TopicsView::clearTopicJumpGeometry() {
|
||||
changeTopicJumpGeometry({});
|
||||
}
|
||||
|
||||
bool TopicsView::isInTopicJumpArea(int x, int y) const {
|
||||
return _lastTopicJumpGeometry.area1.contains(x, y)
|
||||
|| _lastTopicJumpGeometry.area2.contains(x, y);
|
||||
}
|
||||
|
||||
void TopicsView::addTopicJumpRipple(
|
||||
QPoint origin,
|
||||
not_null<TopicJumpCache*> topicJumpCache,
|
||||
Fn<void()> updateCallback) {
|
||||
auto mask = topicJumpRippleMask(topicJumpCache);
|
||||
if (mask.isNull()) {
|
||||
return;
|
||||
}
|
||||
_ripple = std::make_unique<Ui::RippleAnimation>(
|
||||
st::dialogsRipple,
|
||||
std::move(mask),
|
||||
std::move(updateCallback));
|
||||
_ripple->add(origin);
|
||||
}
|
||||
|
||||
void TopicsView::stopLastRipple() {
|
||||
if (_ripple) {
|
||||
_ripple->lastStop();
|
||||
}
|
||||
}
|
||||
|
||||
void TopicsView::clearRipple() {
|
||||
_ripple = nullptr;
|
||||
}
|
||||
|
||||
void TopicsView::paintRipple(
|
||||
QPainter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
const QColor *colorOverride) const {
|
||||
if (_ripple) {
|
||||
_ripple->paint(p, x, y, outerWidth, colorOverride);
|
||||
if (_ripple->empty()) {
|
||||
_ripple.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QImage TopicsView::topicJumpRippleMask(
|
||||
not_null<TopicJumpCache*> topicJumpCache) const {
|
||||
const auto &st = st::forumDialogRow;
|
||||
const auto area1 = _lastTopicJumpGeometry.area1;
|
||||
if (area1.isEmpty()) {
|
||||
return QImage();
|
||||
}
|
||||
const auto area2 = _lastTopicJumpGeometry.area2;
|
||||
const auto drawer = [&](QPainter &p) {
|
||||
const auto white = style::complex_color([] { return Qt::white; });
|
||||
// p.setOpacity(.1);
|
||||
FillJumpToLastPrepared(p, {
|
||||
.st = &st,
|
||||
.corners = &topicJumpCache->rippleMask,
|
||||
.bg = white.color(),
|
||||
.prepared = _lastTopicJumpGeometry,
|
||||
});
|
||||
};
|
||||
return Ui::RippleAnimation::MaskByDrawer(
|
||||
QRect(0, 0, 1, 1).united(area1).united(area2).size(),
|
||||
false,
|
||||
drawer);
|
||||
}
|
||||
|
||||
JumpToLastGeometry FillJumpToLastBg(QPainter &p, JumpToLastBg context) {
|
||||
const auto padding = st::forumDialogJumpPadding;
|
||||
const auto availableWidth = context.geometry.width();
|
||||
const auto want1 = std::min(context.width1, availableWidth);
|
||||
const auto use1 = std::min(want1, availableWidth - padding.right());
|
||||
const auto use2 = std::min(context.width2, availableWidth);
|
||||
const auto rightCut = want1 - use1;
|
||||
const auto origin = context.geometry.topLeft();
|
||||
const auto delta = std::abs(use1 - use2);
|
||||
if (delta <= context.st->topicsSkip / 2) {
|
||||
const auto w = std::max(use1, use2);
|
||||
const auto h = context.st->topicsHeight + st::normalFont->height;
|
||||
const auto fill = QRect(origin, QSize(w, h));
|
||||
const auto full = fill.marginsAdded(padding);
|
||||
auto result = JumpToLastGeometry{ rightCut, full };
|
||||
FillJumpToLastPrepared(p, {
|
||||
.st = context.st,
|
||||
.corners = context.corners,
|
||||
.bg = context.bg,
|
||||
.prepared = result,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
const auto h1 = context.st->topicsHeight;
|
||||
const auto h2 = st::normalFont->height;
|
||||
const auto rect1 = QRect(origin, QSize(use1, h1));
|
||||
const auto fill1 = rect1.marginsAdded({
|
||||
padding.left(),
|
||||
padding.top(),
|
||||
padding.right(),
|
||||
(use1 < use2 ? -padding.top() : padding.bottom()),
|
||||
});
|
||||
const auto add = QPoint(0, h1);
|
||||
const auto rect2 = QRect(origin + add, QSize(use2, h2));
|
||||
const auto fill2 = rect2.marginsAdded({
|
||||
padding.left(),
|
||||
(use2 < use1 ? -padding.bottom() : padding.top()),
|
||||
padding.right(),
|
||||
padding.bottom(),
|
||||
});
|
||||
auto result = JumpToLastGeometry{ rightCut, fill1, fill2 };
|
||||
FillJumpToLastPrepared(p, {
|
||||
.st = context.st,
|
||||
.corners = context.corners,
|
||||
.bg = context.bg,
|
||||
.prepared = result,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
void FillJumpToLastPrepared(QPainter &p, JumpToLastPrepared context) {
|
||||
auto &normal = context.corners->normal;
|
||||
auto &inverted = context.corners->inverted;
|
||||
auto &small = context.corners->small;
|
||||
const auto radius = st::forumDialogJumpRadius;
|
||||
const auto &bg = context.bg;
|
||||
const auto area1 = context.prepared.area1;
|
||||
const auto area2 = context.prepared.area2;
|
||||
if (area2.isNull()) {
|
||||
if (normal.p[0].isNull()) {
|
||||
normal = Ui::PrepareCornerPixmaps(radius, bg);
|
||||
}
|
||||
Ui::FillRoundRect(p, area1, bg, normal);
|
||||
return;
|
||||
}
|
||||
const auto width1 = area1.width();
|
||||
const auto width2 = area2.width();
|
||||
const auto delta = std::abs(width1 - width2);
|
||||
const auto h1 = context.st->topicsHeight;
|
||||
const auto h2 = st::normalFont->height;
|
||||
const auto hmin = std::min(h1, h2);
|
||||
const auto wantedInvertedRadius = hmin - radius;
|
||||
const auto invertedr = std::min(wantedInvertedRadius, delta / 2);
|
||||
const auto smallr = std::min(radius, delta - invertedr);
|
||||
const auto smallkey = (width1 < width2) ? smallr : (-smallr);
|
||||
if (normal.p[0].isNull()) {
|
||||
normal = Ui::PrepareCornerPixmaps(radius, bg);
|
||||
}
|
||||
if (inverted.p[0].isNull()
|
||||
|| context.corners->invertedRadius != invertedr) {
|
||||
context.corners->invertedRadius = invertedr;
|
||||
inverted = Ui::PrepareInvertedCornerPixmaps(invertedr, bg);
|
||||
}
|
||||
if (smallr != radius
|
||||
&& (small.isNull() || context.corners->smallKey != smallkey)) {
|
||||
context.corners->smallKey = smallr;
|
||||
auto pixmaps = Ui::PrepareCornerPixmaps(smallr, bg);
|
||||
small = pixmaps.p[(width1 < width2) ? 1 : 3];
|
||||
}
|
||||
auto no1 = normal;
|
||||
no1.p[2] = QPixmap();
|
||||
if (width1 < width2) {
|
||||
no1.p[3] = QPixmap();
|
||||
} else if (smallr != radius) {
|
||||
no1.p[3] = small;
|
||||
}
|
||||
Ui::FillRoundRect(p, area1, bg, no1);
|
||||
if (width1 < width2) {
|
||||
p.drawPixmap(
|
||||
area1.x() + width1,
|
||||
area1.y() + area1.height() - invertedr,
|
||||
inverted.p[3]);
|
||||
}
|
||||
auto no2 = normal;
|
||||
no2.p[0] = QPixmap();
|
||||
if (width2 < width1) {
|
||||
no2.p[1] = QPixmap();
|
||||
} else if (smallr != radius) {
|
||||
no2.p[1] = small;
|
||||
}
|
||||
Ui::FillRoundRect(p, area2, bg, no2);
|
||||
if (width2 < width1) {
|
||||
p.drawPixmap(
|
||||
area2.x() + width2,
|
||||
area2.y(),
|
||||
inverted.p[0]);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Dialogs::Ui
|
||||
130
Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.h
Normal file
130
Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.h
Normal file
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
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 Painter;
|
||||
|
||||
namespace style {
|
||||
struct DialogRow;
|
||||
} // namespace style
|
||||
|
||||
namespace Data {
|
||||
class Forum;
|
||||
class ForumTopic;
|
||||
class SavedMessages;
|
||||
class SavedSublist;
|
||||
} // namespace Data
|
||||
|
||||
namespace Ui {
|
||||
class RippleAnimation;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Dialogs::Ui {
|
||||
|
||||
using namespace ::Ui;
|
||||
|
||||
struct PaintContext;
|
||||
struct TopicJumpCache;
|
||||
struct TopicJumpCorners;
|
||||
|
||||
struct JumpToLastBg {
|
||||
not_null<const style::DialogRow*> st;
|
||||
not_null<TopicJumpCorners*> corners;
|
||||
QRect geometry;
|
||||
const style::color &bg;
|
||||
int width1 = 0;
|
||||
int width2 = 0;
|
||||
};
|
||||
struct JumpToLastGeometry {
|
||||
int rightCut = 0;
|
||||
QRect area1;
|
||||
QRect area2;
|
||||
|
||||
friend inline bool operator==(
|
||||
const JumpToLastGeometry&,
|
||||
const JumpToLastGeometry&) = default;
|
||||
};
|
||||
JumpToLastGeometry FillJumpToLastBg(QPainter &p, JumpToLastBg context);
|
||||
|
||||
struct JumpToLastPrepared {
|
||||
not_null<const style::DialogRow*> st;
|
||||
not_null<TopicJumpCorners*> corners;
|
||||
const style::color &bg;
|
||||
const JumpToLastGeometry &prepared;
|
||||
};
|
||||
void FillJumpToLastPrepared(QPainter &p, JumpToLastPrepared context);
|
||||
|
||||
class TopicsView final {
|
||||
public:
|
||||
TopicsView(Data::Forum *forum, Data::SavedMessages *monoforum);
|
||||
~TopicsView();
|
||||
|
||||
[[nodiscard]] Data::Forum *forum() const {
|
||||
return _forum;
|
||||
}
|
||||
[[nodiscard]] Data::SavedMessages *monoforum() const {
|
||||
return _monoforum;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool prepared() const;
|
||||
void prepare(MsgId frontRootId, Fn<void()> customEmojiRepaint);
|
||||
void prepare(PeerId frontPeerId, Fn<void()> customEmojiRepaint);
|
||||
|
||||
[[nodiscard]] int jumpToTopicWidth() const;
|
||||
|
||||
void paint(
|
||||
Painter &p,
|
||||
const QRect &geometry,
|
||||
const PaintContext &context) const;
|
||||
|
||||
bool changeTopicJumpGeometry(JumpToLastGeometry geometry);
|
||||
void clearTopicJumpGeometry();
|
||||
[[nodiscard]] bool isInTopicJumpArea(int x, int y) const;
|
||||
void addTopicJumpRipple(
|
||||
QPoint origin,
|
||||
not_null<TopicJumpCache*> topicJumpCache,
|
||||
Fn<void()> updateCallback);
|
||||
void paintRipple(
|
||||
QPainter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
const QColor *colorOverride) const;
|
||||
void stopLastRipple();
|
||||
void clearRipple();
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
private:
|
||||
struct Title {
|
||||
Text::String title;
|
||||
uint64 key = 0;
|
||||
int version = -1;
|
||||
bool unread = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] QImage topicJumpRippleMask(
|
||||
not_null<TopicJumpCache*> topicJumpCache) const;
|
||||
|
||||
Data::Forum * const _forum = nullptr;
|
||||
Data::SavedMessages * const _monoforum = nullptr;
|
||||
|
||||
mutable std::vector<Title> _titles;
|
||||
mutable std::unique_ptr<RippleAnimation> _ripple;
|
||||
JumpToLastGeometry _lastTopicJumpGeometry;
|
||||
int _version = -1;
|
||||
bool _jumpToTopic = false;
|
||||
bool _allLoaded = false;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Dialogs::Ui
|
||||
175
Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.cpp
Normal file
175
Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.cpp
Normal file
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
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 "dialogs/ui/dialogs_video_userpic.h"
|
||||
|
||||
#include "core/file_location.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_photo.h"
|
||||
#include "data/data_photo_media.h"
|
||||
#include "data/data_file_origin.h"
|
||||
#include "data/data_session.h"
|
||||
#include "dialogs/dialogs_entry.h"
|
||||
#include "dialogs/ui/dialogs_layout.h"
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
|
||||
namespace Dialogs::Ui {
|
||||
|
||||
VideoUserpic::VideoUserpic(not_null<PeerData*> peer, Fn<void()> repaint)
|
||||
: _peer(peer)
|
||||
, _repaint(std::move(repaint)) {
|
||||
}
|
||||
|
||||
VideoUserpic::~VideoUserpic() = default;
|
||||
|
||||
int VideoUserpic::frameIndex() const {
|
||||
return -1;
|
||||
}
|
||||
|
||||
void VideoUserpic::paintLeft(
|
||||
Painter &p,
|
||||
Ui::PeerUserpicView &view,
|
||||
int x,
|
||||
int y,
|
||||
int w,
|
||||
int size,
|
||||
bool paused) {
|
||||
_lastSize = size;
|
||||
|
||||
const auto photoId = _peer->userpicPhotoId();
|
||||
if (_videoPhotoId != photoId) {
|
||||
_videoPhotoId = photoId;
|
||||
_video = nullptr;
|
||||
_videoPhotoMedia = nullptr;
|
||||
const auto photo = _peer->owner().photo(photoId);
|
||||
if (photo->isNull()) {
|
||||
_peer->updateFullForced();
|
||||
} else {
|
||||
_videoPhotoMedia = photo->createMediaView();
|
||||
_videoPhotoMedia->videoWanted(
|
||||
Data::PhotoSize::Small,
|
||||
_peer->userpicPhotoOrigin());
|
||||
}
|
||||
}
|
||||
if (!_video) {
|
||||
if (!_videoPhotoMedia) {
|
||||
const auto photo = _peer->owner().photo(photoId);
|
||||
if (!photo->isNull()) {
|
||||
_videoPhotoMedia = photo->createMediaView();
|
||||
_videoPhotoMedia->videoWanted(
|
||||
Data::PhotoSize::Small,
|
||||
_peer->userpicPhotoOrigin());
|
||||
}
|
||||
}
|
||||
if (_videoPhotoMedia) {
|
||||
auto small = _videoPhotoMedia->videoContent(
|
||||
Data::PhotoSize::Small);
|
||||
auto bytes = small.isEmpty()
|
||||
? _videoPhotoMedia->videoContent(Data::PhotoSize::Large)
|
||||
: small;
|
||||
if (!bytes.isEmpty()) {
|
||||
auto callback = [=](Media::Clip::Notification notification) {
|
||||
clipCallback(notification);
|
||||
};
|
||||
_video = Media::Clip::MakeReader(
|
||||
Core::FileLocation(),
|
||||
std::move(bytes),
|
||||
std::move(callback));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rtl()) {
|
||||
x = w - x - size;
|
||||
}
|
||||
if (_video && _video->ready()) {
|
||||
startReady();
|
||||
|
||||
const auto now = paused ? crl::time(0) : crl::now();
|
||||
p.drawImage(x, y, _video->current(request(size), now));
|
||||
} else {
|
||||
_peer->paintUserpicLeft(p, view, x, y, w, size);
|
||||
}
|
||||
}
|
||||
|
||||
Media::Clip::FrameRequest VideoUserpic::request(int size) const {
|
||||
return {
|
||||
.frame = { size, size },
|
||||
.outer = { size, size },
|
||||
.factor = style::DevicePixelRatio(),
|
||||
.radius = ImageRoundRadius::Ellipse,
|
||||
};
|
||||
}
|
||||
|
||||
bool VideoUserpic::startReady(int size) {
|
||||
if (!_video->ready() || _video->started()) {
|
||||
return false;
|
||||
} else if (!_lastSize) {
|
||||
_lastSize = size ? size : _video->width();
|
||||
}
|
||||
_video->start(request(_lastSize));
|
||||
_repaint();
|
||||
return true;
|
||||
}
|
||||
|
||||
void VideoUserpic::clipCallback(Media::Clip::Notification notification) {
|
||||
using namespace Media::Clip;
|
||||
|
||||
switch (notification) {
|
||||
case Notification::Reinit: {
|
||||
if (_video->state() == State::Error) {
|
||||
_video.setBad();
|
||||
} else if (startReady()) {
|
||||
_repaint();
|
||||
}
|
||||
} break;
|
||||
|
||||
case Notification::Repaint: _repaint(); break;
|
||||
}
|
||||
}
|
||||
|
||||
void PaintUserpic(
|
||||
Painter &p,
|
||||
not_null<Entry*> entry,
|
||||
PeerData *peer,
|
||||
VideoUserpic *videoUserpic,
|
||||
PeerUserpicView &view,
|
||||
const Ui::PaintContext &context) {
|
||||
if (peer) {
|
||||
PaintUserpic(
|
||||
p,
|
||||
peer,
|
||||
videoUserpic,
|
||||
view,
|
||||
context.st->padding.left(),
|
||||
context.st->padding.top(),
|
||||
context.width,
|
||||
context.st->photoSize,
|
||||
context.paused);
|
||||
} else {
|
||||
entry->paintUserpic(p, view, context);
|
||||
}
|
||||
}
|
||||
|
||||
void PaintUserpic(
|
||||
Painter &p,
|
||||
not_null<PeerData*> peer,
|
||||
Ui::VideoUserpic *videoUserpic,
|
||||
Ui::PeerUserpicView &view,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
int size,
|
||||
bool paused) {
|
||||
if (videoUserpic) {
|
||||
videoUserpic->paintLeft(p, view, x, y, outerWidth, size, paused);
|
||||
} else {
|
||||
peer->paintUserpicLeft(p, view, x, y, outerWidth, size);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Dialogs::Ui
|
||||
82
Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.h
Normal file
82
Telegram/SourceFiles/dialogs/ui/dialogs_video_userpic.h
Normal file
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
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 "media/clip/media_clip_reader.h"
|
||||
|
||||
class Painter;
|
||||
|
||||
namespace Data {
|
||||
class PhotoMedia;
|
||||
} // namespace Data
|
||||
|
||||
namespace Ui {
|
||||
struct PeerUserpicView;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Dialogs {
|
||||
class Entry;
|
||||
} // namespace Dialogs
|
||||
|
||||
namespace Dialogs::Ui {
|
||||
|
||||
using namespace ::Ui;
|
||||
|
||||
struct PaintContext;
|
||||
|
||||
class VideoUserpic final {
|
||||
public:
|
||||
VideoUserpic(not_null<PeerData*> peer, Fn<void()> repaint);
|
||||
~VideoUserpic();
|
||||
|
||||
[[nodiscard]] int frameIndex() const;
|
||||
|
||||
void paintLeft(
|
||||
Painter &p,
|
||||
PeerUserpicView &view,
|
||||
int x,
|
||||
int y,
|
||||
int w,
|
||||
int size,
|
||||
bool paused);
|
||||
|
||||
private:
|
||||
void clipCallback(Media::Clip::Notification notification);
|
||||
[[nodiscard]] Media::Clip::FrameRequest request(int size) const;
|
||||
bool startReady(int size = 0);
|
||||
|
||||
const not_null<PeerData*> _peer;
|
||||
const Fn<void()> _repaint;
|
||||
|
||||
Media::Clip::ReaderPointer _video;
|
||||
int _lastSize = 0;
|
||||
std::shared_ptr<Data::PhotoMedia> _videoPhotoMedia;
|
||||
PhotoId _videoPhotoId = 0;
|
||||
|
||||
};
|
||||
|
||||
void PaintUserpic(
|
||||
Painter &p,
|
||||
not_null<Entry*> entry,
|
||||
PeerData *peer,
|
||||
VideoUserpic *videoUserpic,
|
||||
PeerUserpicView &view,
|
||||
const Ui::PaintContext &context);
|
||||
|
||||
void PaintUserpic(
|
||||
Painter &p,
|
||||
not_null<PeerData*> peer,
|
||||
VideoUserpic *videoUserpic,
|
||||
PeerUserpicView &view,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
int size,
|
||||
bool paused);
|
||||
|
||||
} // namespace Dialogs::Ui
|
||||
253
Telegram/SourceFiles/dialogs/ui/posts_search_intro.cpp
Normal file
253
Telegram/SourceFiles/dialogs/ui/posts_search_intro.cpp
Normal file
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
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 "dialogs/ui/posts_search_intro.h"
|
||||
|
||||
#include "base/timer_rpl.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/controls/button_labels.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "styles/style_credits.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
|
||||
namespace Dialogs {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] rpl::producer<QString> FormatCountdownTill(TimeId when) {
|
||||
return rpl::single(rpl::empty) | rpl::then(
|
||||
base::timer_each(1000)
|
||||
) | rpl::map([=] {
|
||||
const auto now = base::unixtime::now();
|
||||
const auto delta = std::max(when - now, 0);
|
||||
const auto hours = delta / 3600;
|
||||
const auto minutes = (delta % 3600) / 60;
|
||||
const auto seconds = delta % 60;
|
||||
constexpr auto kZero = QChar('0');
|
||||
return (hours > 0)
|
||||
? u"%1:%2:%3"_q
|
||||
.arg(hours)
|
||||
.arg(minutes, 2, 10, kZero)
|
||||
.arg(seconds, 2, 10, kZero)
|
||||
: u"%1:%2"_q
|
||||
.arg(minutes)
|
||||
.arg(seconds, 2, 10, kZero);
|
||||
});
|
||||
}
|
||||
|
||||
void SetSearchButtonLabel(
|
||||
not_null<Ui::RpWidget*> button,
|
||||
rpl::producer<TextWithEntities> text) {
|
||||
const auto left = &st::postsSearchIcon;
|
||||
const auto leftPadding = st::postsSearchIconPadding;
|
||||
const auto right = &st::postsSearchArrow;
|
||||
const auto rightPadding = st::postsSearchArrowPadding;
|
||||
const auto leftSkip = left->size().grownBy(leftPadding).width();
|
||||
const auto rightSkip = right->size().grownBy(rightPadding).width();
|
||||
|
||||
struct State {
|
||||
State() : linkFg([] {
|
||||
auto copy = st::windowFgActive->c;
|
||||
copy.setAlphaF(0.6);
|
||||
return copy;
|
||||
}), st(st::resaleButtonTitle) {
|
||||
}
|
||||
|
||||
style::complex_color linkFg;
|
||||
style::FlatLabel st;
|
||||
};
|
||||
auto lifetime = rpl::lifetime();
|
||||
const auto state = lifetime.make_state<State>();
|
||||
state->st.palette.linkFg = state->linkFg.color();
|
||||
|
||||
const auto label = Ui::CreateChild<Ui::FlatLabel>(
|
||||
button,
|
||||
rpl::duplicate(text),
|
||||
state->st);
|
||||
label->lifetime().add(std::move(lifetime));
|
||||
label->show();
|
||||
|
||||
const auto icons = Ui::CreateChild<Ui::RpWidget>(button);
|
||||
icons->show();
|
||||
rpl::combine(
|
||||
button->sizeValue(),
|
||||
std::move(text)
|
||||
) | rpl::on_next([=](QSize size, const auto &) {
|
||||
icons->setGeometry(QRect(QPoint(), size));
|
||||
const auto available = size.width() - leftSkip - rightSkip;
|
||||
if (available <= 0) {
|
||||
return;
|
||||
}
|
||||
const auto width = std::min(available, label->textMaxWidth());
|
||||
label->resizeToWidth(width);
|
||||
const auto full = leftSkip + width + rightSkip;
|
||||
const auto x = (size.width() - full) / 2;
|
||||
const auto y = (size.height() - label->height()) / 2;
|
||||
label->moveToLeft(x + leftSkip, y, size.width());
|
||||
}, icons->lifetime());
|
||||
|
||||
icons->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(icons);
|
||||
left->paint(
|
||||
p,
|
||||
label->x() - leftSkip + leftPadding.left(),
|
||||
label->y() + leftPadding.top(),
|
||||
icons->width());
|
||||
right->paint(
|
||||
p,
|
||||
label->x() + label->width() + rightPadding.left(),
|
||||
label->y() + rightPadding.top(),
|
||||
icons->width());
|
||||
}, icons->lifetime());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PostsSearchIntro::PostsSearchIntro(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
PostsSearchIntroState state)
|
||||
: RpWidget(parent)
|
||||
, _state(std::move(state))
|
||||
, _content(std::make_unique<Ui::VerticalLayout>(this)) {
|
||||
setup();
|
||||
}
|
||||
|
||||
PostsSearchIntro::~PostsSearchIntro() = default;
|
||||
|
||||
void PostsSearchIntro::update(PostsSearchIntroState state) {
|
||||
_state = std::move(state);
|
||||
}
|
||||
|
||||
rpl::producer<int> PostsSearchIntro::searchWithStars() const {
|
||||
return _button->clicks() | rpl::map([=] {
|
||||
const auto &now = _state.current();
|
||||
return (now.needsPremium || now.freeSearchesLeft)
|
||||
? 0
|
||||
: int(now.starsPerPaidSearch);
|
||||
});
|
||||
}
|
||||
|
||||
void PostsSearchIntro::setup() {
|
||||
auto title = _state.value(
|
||||
) | rpl::map([](const PostsSearchIntroState &state) {
|
||||
return (state.needsPremium || state.freeSearchesLeft > 0)
|
||||
? tr::lng_posts_title()
|
||||
: tr::lng_posts_limit_reached();
|
||||
}) | rpl::flatten_latest();
|
||||
|
||||
auto subtitle = _state.value(
|
||||
) | rpl::map([](const PostsSearchIntroState &state) {
|
||||
return (state.needsPremium || state.freeSearchesLeft > 0)
|
||||
? tr::lng_posts_start()
|
||||
: tr::lng_posts_limit_about(
|
||||
lt_count,
|
||||
rpl::single(state.freeSearchesPerDay * 1.));
|
||||
}) | rpl::flatten_latest();
|
||||
|
||||
auto footer = _state.value(
|
||||
) | rpl::map([](const PostsSearchIntroState &state)
|
||||
-> rpl::producer<QString> {
|
||||
if (state.needsPremium) {
|
||||
return tr::lng_posts_need_subscribe();
|
||||
} else if (state.freeSearchesLeft > 0) {
|
||||
return tr::lng_posts_remaining(
|
||||
lt_count,
|
||||
rpl::single(state.freeSearchesLeft * 1.));
|
||||
} else {
|
||||
return rpl::single(QString());
|
||||
}
|
||||
}) | rpl::flatten_latest();
|
||||
|
||||
_title = _content->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
_content.get(),
|
||||
std::move(title),
|
||||
st::postsSearchIntroTitle),
|
||||
st::postsSearchIntroTitleMargin,
|
||||
style::al_top);
|
||||
_title->setTryMakeSimilarLines(true);
|
||||
_subtitle = _content->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
_content.get(),
|
||||
std::move(subtitle),
|
||||
st::postsSearchIntroSubtitle),
|
||||
st::postsSearchIntroSubtitleMargin,
|
||||
style::al_top);
|
||||
_subtitle->setTryMakeSimilarLines(true);
|
||||
_button = _content->add(
|
||||
object_ptr<Ui::RoundButton>(
|
||||
_content.get(),
|
||||
rpl::single(QString()),
|
||||
st::postsSearchIntroButton),
|
||||
style::al_top);
|
||||
_button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
|
||||
_footer = _content->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
_content.get(),
|
||||
std::move(footer),
|
||||
st::postsSearchIntroFooter),
|
||||
st::postsSearchIntroFooterMargin,
|
||||
style::al_top);
|
||||
_footer->setTryMakeSimilarLines(true);
|
||||
|
||||
_state.value(
|
||||
) | rpl::on_next([=](const PostsSearchIntroState &state) {
|
||||
if (state.query.trimmed().isEmpty() && !state.needsPremium) {
|
||||
_button->resize(_button->width(), 0);
|
||||
_content->resizeToWidth(width());
|
||||
return;
|
||||
}
|
||||
|
||||
auto copy = _button->children();
|
||||
for (const auto child : copy) {
|
||||
delete child;
|
||||
}
|
||||
if (state.needsPremium) {
|
||||
_button->setText(tr::lng_posts_subscribe());
|
||||
} else if (state.freeSearchesLeft > 0) {
|
||||
_button->setText(rpl::single(QString()));
|
||||
|
||||
SetSearchButtonLabel(_button, tr::lng_posts_search_button(
|
||||
lt_query,
|
||||
rpl::single(Ui::Text::Colorized(state.query.trimmed())),
|
||||
tr::marked));
|
||||
} else {
|
||||
_button->setText(rpl::single(QString()));
|
||||
|
||||
Ui::SetButtonTwoLabels(
|
||||
_button,
|
||||
tr::lng_posts_limit_search_paid(
|
||||
lt_cost,
|
||||
rpl::single(Ui::Text::IconEmoji(
|
||||
&st::starIconEmoji
|
||||
).append(
|
||||
Lang::FormatCountDecimal(state.starsPerPaidSearch))),
|
||||
tr::marked),
|
||||
tr::lng_posts_limit_unlocks(
|
||||
lt_duration,
|
||||
FormatCountdownTill(
|
||||
state.nextFreeSearchTime
|
||||
) | rpl::map(tr::marked),
|
||||
tr::marked),
|
||||
st::resaleButtonTitle,
|
||||
st::resaleButtonSubtitle);
|
||||
}
|
||||
_button->resize(_button->width(), st::postsSearchIntroButton.height);
|
||||
_content->resizeToWidth(width());
|
||||
}, _button->lifetime());
|
||||
}
|
||||
|
||||
void PostsSearchIntro::resizeEvent(QResizeEvent *e) {
|
||||
_content->resizeToWidth(width());
|
||||
const auto top = std::max(0, (height() - _content->height()) / 3);
|
||||
_content->move(0, top);
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
59
Telegram/SourceFiles/dialogs/ui/posts_search_intro.h
Normal file
59
Telegram/SourceFiles/dialogs/ui/posts_search_intro.h
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
namespace Ui {
|
||||
class FlatLabel;
|
||||
class RoundButton;
|
||||
class VerticalLayout;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
struct PostsSearchIntroState {
|
||||
QString query;
|
||||
int freeSearchesPerDay = 0;
|
||||
int freeSearchesLeft = 0;
|
||||
TimeId nextFreeSearchTime = 0;
|
||||
uint32 starsPerPaidSearch : 31 = 0;
|
||||
uint32 needsPremium : 1 = 0;
|
||||
|
||||
friend inline bool operator==(
|
||||
PostsSearchIntroState,
|
||||
PostsSearchIntroState) = default;
|
||||
};
|
||||
|
||||
class PostsSearchIntro final : public Ui::RpWidget {
|
||||
public:
|
||||
PostsSearchIntro(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
PostsSearchIntroState state);
|
||||
~PostsSearchIntro();
|
||||
|
||||
void update(PostsSearchIntroState state);
|
||||
|
||||
[[nodiscard]] rpl::producer<int> searchWithStars() const;
|
||||
|
||||
private:
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
void setup();
|
||||
|
||||
rpl::variable<PostsSearchIntroState> _state;
|
||||
|
||||
std::unique_ptr<Ui::VerticalLayout> _content;
|
||||
Ui::FlatLabel *_title = nullptr;
|
||||
Ui::FlatLabel *_subtitle = nullptr;
|
||||
Ui::RoundButton *_button = nullptr;
|
||||
Ui::FlatLabel *_footer = nullptr;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Dialogs
|
||||
995
Telegram/SourceFiles/dialogs/ui/top_peers_strip.cpp
Normal file
995
Telegram/SourceFiles/dialogs/ui/top_peers_strip.cpp
Normal file
@@ -0,0 +1,995 @@
|
||||
/*
|
||||
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 "dialogs/ui/top_peers_strip.h"
|
||||
|
||||
#include "base/event_filter.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/text/text.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/dynamic_image.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/unread_badge_paint.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
#include <QtWidgets/QApplication>
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
struct TopPeersStrip::Entry {
|
||||
uint64 id = 0;
|
||||
Ui::Text::String name;
|
||||
std::shared_ptr<Ui::DynamicImage> userpic;
|
||||
std::unique_ptr<Ui::RippleAnimation> ripple;
|
||||
Ui::Animations::Simple onlineShown;
|
||||
QImage userpicFrame;
|
||||
float64 userpicFrameOnline = 0.;
|
||||
QString badgeString;
|
||||
uint32 badge : 27 = 0;
|
||||
uint32 userpicFrameDirty : 1 = 0;
|
||||
uint32 subscribed : 1 = 0;
|
||||
uint32 unread : 1 = 0;
|
||||
uint32 online : 1 = 0;
|
||||
uint32 muted : 1 = 0;
|
||||
};
|
||||
|
||||
struct TopPeersStrip::Layout {
|
||||
int single = 0;
|
||||
int inrow = 0;
|
||||
float64 fsingle = 0.;
|
||||
float64 added = 0.;
|
||||
};
|
||||
|
||||
TopPeersStrip::TopPeersStrip(
|
||||
not_null<QWidget*> parent,
|
||||
rpl::producer<TopPeersList> content)
|
||||
: RpWidget(parent)
|
||||
, _header(this)
|
||||
, _strip(this)
|
||||
, _selection(st::topPeersRadius, st::windowBgOver) {
|
||||
setupHeader();
|
||||
setupStrip();
|
||||
|
||||
std::move(content) | rpl::on_next([=](const TopPeersList &list) {
|
||||
apply(list);
|
||||
}, lifetime());
|
||||
|
||||
rpl::combine(
|
||||
_count.value(),
|
||||
_expanded.value()
|
||||
) | rpl::on_next([=] {
|
||||
resizeToWidth(width());
|
||||
}, _strip.lifetime());
|
||||
|
||||
resize(0, _header.height() + _strip.height());
|
||||
}
|
||||
|
||||
void TopPeersStrip::setupHeader() {
|
||||
_header.resize(0, st::searchedBarHeight);
|
||||
|
||||
const auto label = Ui::CreateChild<Ui::FlatLabel>(
|
||||
&_header,
|
||||
tr::lng_recent_frequent(),
|
||||
st::searchedBarLabel);
|
||||
const auto single = outer().width();
|
||||
|
||||
rpl::combine(
|
||||
_count.value(),
|
||||
widthValue()
|
||||
) | rpl::map(
|
||||
(rpl::mappers::_1 * single) > (rpl::mappers::_2 + (single * 2) / 3)
|
||||
) | rpl::distinct_until_changed() | rpl::on_next([=](bool more) {
|
||||
setExpanded(false);
|
||||
if (!more) {
|
||||
const auto toggle = _toggleExpanded.current();
|
||||
_toggleExpanded = nullptr;
|
||||
delete toggle;
|
||||
return;
|
||||
} else if (_toggleExpanded.current()) {
|
||||
return;
|
||||
}
|
||||
const auto toggle = Ui::CreateChild<Ui::LinkButton>(
|
||||
&_header,
|
||||
tr::lng_channels_your_more(tr::now),
|
||||
st::searchedBarLink);
|
||||
toggle->show();
|
||||
toggle->setClickedCallback([=] {
|
||||
const auto expand = !_expanded.current();
|
||||
toggle->setText(expand
|
||||
? tr::lng_recent_frequent_collapse(tr::now)
|
||||
: tr::lng_recent_frequent_all(tr::now));
|
||||
setExpanded(expand);
|
||||
});
|
||||
rpl::combine(
|
||||
_header.sizeValue(),
|
||||
toggle->widthValue()
|
||||
) | rpl::on_next([=](QSize size, int width) {
|
||||
const auto x = st::searchedBarPosition.x();
|
||||
const auto y = st::searchedBarPosition.y();
|
||||
toggle->moveToRight(0, 0, size.width());
|
||||
label->resizeToWidth(size.width() - x - width);
|
||||
label->moveToLeft(x, y, size.width());
|
||||
}, toggle->lifetime());
|
||||
_toggleExpanded = toggle;
|
||||
}, _header.lifetime());
|
||||
|
||||
rpl::combine(
|
||||
_header.sizeValue(),
|
||||
_toggleExpanded.value()
|
||||
) | rpl::filter(
|
||||
rpl::mappers::_2 == nullptr
|
||||
) | rpl::on_next([=](QSize size, const auto) {
|
||||
const auto x = st::searchedBarPosition.x();
|
||||
const auto y = st::searchedBarPosition.y();
|
||||
label->resizeToWidth(size.width() - x * 2);
|
||||
label->moveToLeft(x, y, size.width());
|
||||
}, _header.lifetime());
|
||||
|
||||
_header.paintRequest() | rpl::on_next([=](QRect clip) {
|
||||
QPainter(&_header).fillRect(clip, st::searchedBarBg);
|
||||
}, _header.lifetime());
|
||||
}
|
||||
|
||||
void TopPeersStrip::setExpanded(bool expanded) {
|
||||
if (_expanded.current() == expanded) {
|
||||
return;
|
||||
}
|
||||
const auto from = expanded ? 0. : 1.;
|
||||
const auto to = expanded ? 1. : 0.;
|
||||
_expandAnimation.start([=] {
|
||||
if (!_expandAnimation.animating()) {
|
||||
updateScrollMax();
|
||||
}
|
||||
resizeToWidth(width());
|
||||
update();
|
||||
}, from, to, st::slideDuration, anim::easeOutQuint);
|
||||
_expanded = expanded;
|
||||
}
|
||||
|
||||
void TopPeersStrip::setupStrip() {
|
||||
_strip.resize(0, st::topPeers.height);
|
||||
|
||||
_strip.setMouseTracking(true);
|
||||
|
||||
base::install_event_filter(&_strip, [=](not_null<QEvent*> e) {
|
||||
const auto type = e->type();
|
||||
if (type == QEvent::Wheel) {
|
||||
stripWheelEvent(static_cast<QWheelEvent*>(e.get()));
|
||||
} else if (type == QEvent::MouseButtonPress) {
|
||||
stripMousePressEvent(static_cast<QMouseEvent*>(e.get()));
|
||||
} else if (type == QEvent::MouseMove) {
|
||||
stripMouseMoveEvent(static_cast<QMouseEvent*>(e.get()));
|
||||
} else if (type == QEvent::MouseButtonRelease) {
|
||||
stripMouseReleaseEvent(static_cast<QMouseEvent*>(e.get()));
|
||||
} else if (type == QEvent::ContextMenu) {
|
||||
stripContextMenuEvent(static_cast<QContextMenuEvent*>(e.get()));
|
||||
} else if (type == QEvent::Leave) {
|
||||
stripLeaveEvent(e.get());
|
||||
} else {
|
||||
return base::EventFilterResult::Continue;
|
||||
}
|
||||
return base::EventFilterResult::Cancel;
|
||||
});
|
||||
|
||||
_strip.paintRequest() | rpl::on_next([=](QRect clip) {
|
||||
paintStrip(clip);
|
||||
}, _strip.lifetime());
|
||||
}
|
||||
|
||||
TopPeersStrip::~TopPeersStrip() {
|
||||
unsubscribeUserpics(true);
|
||||
}
|
||||
|
||||
int TopPeersStrip::resizeGetHeight(int newWidth) {
|
||||
_header.resize(newWidth, _header.height());
|
||||
const auto single = QSize(outer().width(), st::topPeers.height);
|
||||
const auto inRow = newWidth / single.width();
|
||||
const auto rows = (inRow > 0)
|
||||
? ((std::max(_count.current(), 1) + inRow - 1) / inRow)
|
||||
: 1;
|
||||
const auto height = single.height() * rows;
|
||||
const auto value = _expandAnimation.value(_expanded.current() ? 1. : 0.);
|
||||
const auto result = anim::interpolate(single.height(), height, value);
|
||||
_strip.setGeometry(0, _header.height(), newWidth, result);
|
||||
updateScrollMax(newWidth);
|
||||
return _strip.y() + _strip.height();
|
||||
}
|
||||
|
||||
rpl::producer<not_null<QWheelEvent*>> TopPeersStrip::verticalScrollEvents() const {
|
||||
return _verticalScrollEvents.events();
|
||||
}
|
||||
|
||||
void TopPeersStrip::stripWheelEvent(QWheelEvent *e) {
|
||||
const auto phase = e->phase();
|
||||
const auto fullDelta = e->pixelDelta().isNull()
|
||||
? e->angleDelta()
|
||||
: e->pixelDelta();
|
||||
if (phase == Qt::ScrollBegin || phase == Qt::ScrollEnd) {
|
||||
_scrollingLock = Qt::Orientation();
|
||||
if (fullDelta.isNull()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const auto vertical = qAbs(fullDelta.x()) < qAbs(fullDelta.y());
|
||||
if (_scrollingLock == Qt::Orientation() && phase != Qt::NoScrollPhase) {
|
||||
_scrollingLock = vertical ? Qt::Vertical : Qt::Horizontal;
|
||||
}
|
||||
if (_scrollingLock == Qt::Vertical || (vertical && !_scrollLeftMax)) {
|
||||
_verticalScrollEvents.fire(e);
|
||||
return;
|
||||
} else if (_expandAnimation.animating()) {
|
||||
return;
|
||||
}
|
||||
const auto delta = vertical
|
||||
? fullDelta.y()
|
||||
: ((style::RightToLeft() ? -1 : 1) * fullDelta.x());
|
||||
|
||||
const auto now = _scrollLeft;
|
||||
const auto used = now - delta;
|
||||
const auto next = std::clamp(used, 0, _scrollLeftMax);
|
||||
if (next != now) {
|
||||
_scrollLeft = next;
|
||||
unsubscribeUserpics();
|
||||
updateSelected();
|
||||
update();
|
||||
}
|
||||
e->accept();
|
||||
}
|
||||
|
||||
void TopPeersStrip::stripLeaveEvent(QEvent *e) {
|
||||
if (!_selectionByKeyboard) {
|
||||
clearSelection();
|
||||
}
|
||||
if (!_dragging) {
|
||||
_lastMousePosition = std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
void TopPeersStrip::stripMousePressEvent(QMouseEvent *e) {
|
||||
if (e->button() != Qt::LeftButton) {
|
||||
return;
|
||||
}
|
||||
_lastMousePosition = e->globalPos();
|
||||
_selectionByKeyboard = false;
|
||||
updateSelected();
|
||||
|
||||
_mouseDownPosition = _lastMousePosition;
|
||||
_pressed = _selected;
|
||||
|
||||
if (_selected >= 0) {
|
||||
Assert(_selected < _entries.size());
|
||||
auto &entry = _entries[_selected];
|
||||
if (!entry.ripple) {
|
||||
entry.ripple = std::make_unique<Ui::RippleAnimation>(
|
||||
st::defaultRippleAnimation,
|
||||
Ui::RippleAnimation::RoundRectMask(
|
||||
innerRounded().size(),
|
||||
st::topPeersRadius),
|
||||
[=] { update(); });
|
||||
}
|
||||
const auto layout = currentLayout();
|
||||
const auto expanded = _expanded.current();
|
||||
const auto row = expanded ? (_selected / layout.inrow) : 0;
|
||||
const auto column = (_selected - (row * layout.inrow));
|
||||
const auto x = layout.added + column * layout.fsingle - scrollLeft();
|
||||
const auto y = row * st::topPeers.height;
|
||||
entry.ripple->add(e->pos() - QPoint(
|
||||
x + st::topPeersMargin.left(),
|
||||
y + st::topPeersMargin.top()));
|
||||
|
||||
_presses.fire_copy(entry.id);
|
||||
}
|
||||
}
|
||||
|
||||
void TopPeersStrip::stripMouseMoveEvent(QMouseEvent *e) {
|
||||
if (!_lastMousePosition) {
|
||||
_lastMousePosition = e->globalPos();
|
||||
if (_selectionByKeyboard) {
|
||||
return;
|
||||
}
|
||||
} else if (_selectionByKeyboard
|
||||
&& (_lastMousePosition == e->globalPos())) {
|
||||
return;
|
||||
}
|
||||
selectByMouse(e->globalPos());
|
||||
|
||||
if (!_dragging && _mouseDownPosition) {
|
||||
if ((*_lastMousePosition - *_mouseDownPosition).manhattanLength()
|
||||
>= QApplication::startDragDistance()) {
|
||||
_pressCancelled.fire({});
|
||||
if (!_expandAnimation.animating()) {
|
||||
_dragging = true;
|
||||
_startDraggingLeft = _scrollLeft;
|
||||
}
|
||||
}
|
||||
}
|
||||
checkDragging();
|
||||
}
|
||||
|
||||
void TopPeersStrip::selectByMouse(QPoint globalPosition) {
|
||||
_lastMousePosition = globalPosition;
|
||||
_selectionByKeyboard = false;
|
||||
updateSelected();
|
||||
}
|
||||
|
||||
void TopPeersStrip::checkDragging() {
|
||||
if (_dragging && !_expandAnimation.animating()) {
|
||||
const auto sign = (style::RightToLeft() ? -1 : 1);
|
||||
const auto newLeft = std::clamp(
|
||||
(sign * (_mouseDownPosition->x() - _lastMousePosition->x())
|
||||
+ _startDraggingLeft),
|
||||
0,
|
||||
_scrollLeftMax);
|
||||
if (newLeft != _scrollLeft) {
|
||||
_scrollLeft = newLeft;
|
||||
unsubscribeUserpics();
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TopPeersStrip::unsubscribeUserpics(bool all) {
|
||||
if (!all && (_expandAnimation.animating() || _expanded.current())) {
|
||||
return;
|
||||
}
|
||||
const auto single = outer().width();
|
||||
auto x = -_scrollLeft;
|
||||
for (auto &entry : _entries) {
|
||||
if (all || x + single <= 0 || x >= width()) {
|
||||
if (entry.subscribed) {
|
||||
entry.userpic->subscribeToUpdates(nullptr);
|
||||
entry.subscribed = false;
|
||||
}
|
||||
entry.userpicFrame = QImage();
|
||||
entry.onlineShown.stop();
|
||||
entry.ripple = nullptr;
|
||||
}
|
||||
x += single;
|
||||
}
|
||||
}
|
||||
|
||||
void TopPeersStrip::subscribeUserpic(Entry &entry) {
|
||||
const auto raw = entry.userpic.get();
|
||||
entry.userpic->subscribeToUpdates([=] {
|
||||
const auto i = ranges::find(
|
||||
_entries,
|
||||
raw,
|
||||
[&](const Entry &entry) { return entry.userpic.get(); });
|
||||
if (i != end(_entries)) {
|
||||
i->userpicFrameDirty = 1;
|
||||
}
|
||||
update();
|
||||
});
|
||||
entry.subscribed = true;
|
||||
}
|
||||
|
||||
void TopPeersStrip::stripMouseReleaseEvent(QMouseEvent *e) {
|
||||
_pressCancelled.fire({});
|
||||
|
||||
_lastMousePosition = e->globalPos();
|
||||
const auto guard = gsl::finally([&] {
|
||||
_mouseDownPosition = std::nullopt;
|
||||
});
|
||||
|
||||
const auto pressed = clearPressed();
|
||||
if (finishDragging()) {
|
||||
return;
|
||||
}
|
||||
_selectionByKeyboard = false;
|
||||
updateSelected();
|
||||
if (_selected >= 0 && _selected == pressed) {
|
||||
Assert(_selected < _entries.size());
|
||||
_clicks.fire_copy(_entries[_selected].id);
|
||||
}
|
||||
}
|
||||
|
||||
int TopPeersStrip::clearPressed() {
|
||||
const auto pressed = std::exchange(_pressed, -1);
|
||||
if (pressed >= 0) {
|
||||
Assert(pressed < _entries.size());
|
||||
auto &entry = _entries[pressed];
|
||||
if (entry.ripple) {
|
||||
entry.ripple->lastStop();
|
||||
}
|
||||
}
|
||||
return pressed;
|
||||
}
|
||||
|
||||
void TopPeersStrip::updateScrollMax(int newWidth) {
|
||||
if (_expandAnimation.animating()) {
|
||||
return;
|
||||
} else if (!newWidth) {
|
||||
newWidth = width();
|
||||
}
|
||||
if (_expanded.current()) {
|
||||
_scrollLeft = 0;
|
||||
_scrollLeftMax = 0;
|
||||
} else {
|
||||
const auto single = outer().width();
|
||||
const auto widthFull = int(_entries.size()) * single;
|
||||
_scrollLeftMax = std::max(widthFull - newWidth, 0);
|
||||
_scrollLeft = std::clamp(_scrollLeft, 0, _scrollLeftMax);
|
||||
}
|
||||
unsubscribeUserpics();
|
||||
update();
|
||||
}
|
||||
|
||||
bool TopPeersStrip::empty() const {
|
||||
return !_count.current();
|
||||
}
|
||||
|
||||
rpl::producer<bool> TopPeersStrip::emptyValue() const {
|
||||
return _count.value()
|
||||
| rpl::map(!rpl::mappers::_1)
|
||||
| rpl::distinct_until_changed();
|
||||
}
|
||||
|
||||
rpl::producer<uint64> TopPeersStrip::clicks() const {
|
||||
return _clicks.events();
|
||||
}
|
||||
|
||||
rpl::producer<uint64> TopPeersStrip::pressed() const {
|
||||
return _presses.events();
|
||||
}
|
||||
|
||||
rpl::producer<> TopPeersStrip::pressCancelled() const {
|
||||
return _pressCancelled.events();
|
||||
}
|
||||
|
||||
void TopPeersStrip::pressLeftToContextMenu(bool shown) {
|
||||
if (!shown) {
|
||||
_contexted = -1;
|
||||
update();
|
||||
return;
|
||||
}
|
||||
_contexted = clearPressed();
|
||||
if (finishDragging()) {
|
||||
return;
|
||||
}
|
||||
_mouseDownPosition = std::nullopt;
|
||||
}
|
||||
|
||||
auto TopPeersStrip::showMenuRequests() const
|
||||
-> rpl::producer<ShowTopPeerMenuRequest> {
|
||||
return _showMenuRequests.events();
|
||||
}
|
||||
|
||||
auto TopPeersStrip::scrollToRequests() const
|
||||
-> rpl::producer<Ui::ScrollToRequest> {
|
||||
return _scrollToRequests.events();
|
||||
}
|
||||
|
||||
void TopPeersStrip::removeLocally(uint64 id) {
|
||||
if (!id) {
|
||||
unsubscribeUserpics(true);
|
||||
setSelected(-1);
|
||||
_pressed = -1;
|
||||
_entries.clear();
|
||||
_hiddenLocally = true;
|
||||
_count = 0;
|
||||
return;
|
||||
}
|
||||
_removed.emplace(id);
|
||||
const auto i = ranges::find(_entries, id, &Entry::id);
|
||||
if (i == end(_entries)) {
|
||||
return;
|
||||
} else if (i->subscribed) {
|
||||
i->userpic->subscribeToUpdates(nullptr);
|
||||
}
|
||||
const auto index = int(i - begin(_entries));
|
||||
_entries.erase(i);
|
||||
if (_selected > index) {
|
||||
--_selected;
|
||||
}
|
||||
if (_pressed > index) {
|
||||
--_pressed;
|
||||
}
|
||||
if (_contexted > index) {
|
||||
--_contexted;
|
||||
}
|
||||
updateScrollMax();
|
||||
_count = int(_entries.size());
|
||||
update();
|
||||
}
|
||||
|
||||
bool TopPeersStrip::selectedByKeyboard() const {
|
||||
return _selectionByKeyboard && _selected >= 0;
|
||||
}
|
||||
|
||||
bool TopPeersStrip::selectByKeyboard(Qt::Key direction) {
|
||||
if (_entries.empty()) {
|
||||
return false;
|
||||
} else if (direction == Qt::Key()) {
|
||||
_selectionByKeyboard = true;
|
||||
if (_selected < 0) {
|
||||
setSelected(0);
|
||||
scrollToSelected();
|
||||
return true;
|
||||
}
|
||||
} else if (direction == Qt::Key_Left) {
|
||||
if (_selected > 0) {
|
||||
_selectionByKeyboard = true;
|
||||
setSelected(_selected - 1);
|
||||
scrollToSelected();
|
||||
return true;
|
||||
}
|
||||
} else if (direction == Qt::Key_Right) {
|
||||
if (_selected + 1 < _entries.size()) {
|
||||
_selectionByKeyboard = true;
|
||||
setSelected(_selected + 1);
|
||||
scrollToSelected();
|
||||
return true;
|
||||
}
|
||||
} else if (direction == Qt::Key_Up) {
|
||||
const auto layout = currentLayout();
|
||||
if (_selected < 0) {
|
||||
_selectionByKeyboard = true;
|
||||
const auto rows = _expanded.current()
|
||||
? ((int(_entries.size()) + layout.inrow - 1) / layout.inrow)
|
||||
: 1;
|
||||
setSelected((rows - 1) * layout.inrow);
|
||||
scrollToSelected();
|
||||
return true;
|
||||
} else if (!_expanded.current()) {
|
||||
deselectByKeyboard();
|
||||
} else if (_selected >= 0) {
|
||||
const auto row = _selected / layout.inrow;
|
||||
if (row > 0) {
|
||||
_selectionByKeyboard = true;
|
||||
setSelected(_selected - layout.inrow);
|
||||
scrollToSelected();
|
||||
return true;
|
||||
} else {
|
||||
deselectByKeyboard();
|
||||
}
|
||||
}
|
||||
} else if (direction == Qt::Key_Down) {
|
||||
if (_selected >= 0 && _expanded.current()) {
|
||||
const auto layout = currentLayout();
|
||||
const auto row = _selected / layout.inrow;
|
||||
const auto rows = (int(_entries.size()) + layout.inrow - 1)
|
||||
/ layout.inrow;
|
||||
if (row + 1 < rows) {
|
||||
_selectionByKeyboard = true;
|
||||
setSelected(std::min(
|
||||
_selected + layout.inrow,
|
||||
int(_entries.size()) - 1));
|
||||
scrollToSelected();
|
||||
return true;
|
||||
} else {
|
||||
deselectByKeyboard();
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void TopPeersStrip::deselectByKeyboard() {
|
||||
if (_selectionByKeyboard) {
|
||||
setSelected(-1);
|
||||
}
|
||||
}
|
||||
|
||||
bool TopPeersStrip::chooseRow() {
|
||||
if (_selected >= 0) {
|
||||
Assert(_selected < _entries.size());
|
||||
_clicks.fire_copy(_entries[_selected].id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64 TopPeersStrip::updateFromParentDrag(QPoint globalPosition) {
|
||||
if (!rect().contains(mapFromGlobal(globalPosition))) {
|
||||
dragLeft();
|
||||
return 0;
|
||||
}
|
||||
selectByMouse(globalPosition);
|
||||
|
||||
return (_selected >= 0) ? _entries[_selected].id : 0;
|
||||
}
|
||||
|
||||
void TopPeersStrip::dragLeft() {
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
void TopPeersStrip::apply(const TopPeersList &list) {
|
||||
if (_hiddenLocally) {
|
||||
return;
|
||||
}
|
||||
auto now = std::vector<Entry>();
|
||||
|
||||
const auto selectedId = (_selected >= 0) ? _entries[_selected].id : 0;
|
||||
const auto pressedId = (_pressed >= 0) ? _entries[_pressed].id : 0;
|
||||
const auto contextedId = (_contexted >= 0) ? _entries[_contexted].id : 0;
|
||||
const auto restoreIndex = [&](uint64 id) {
|
||||
if (!id) {
|
||||
return -1;
|
||||
}
|
||||
const auto i = ranges::find(_entries, id, &Entry::id);
|
||||
return (i != end(_entries)) ? int(i - begin(_entries)) : -1;
|
||||
};
|
||||
|
||||
for (const auto &entry : list.entries) {
|
||||
if (_removed.contains(entry.id)) {
|
||||
continue;
|
||||
}
|
||||
const auto i = ranges::find(_entries, entry.id, &Entry::id);
|
||||
if (i != end(_entries)) {
|
||||
now.push_back(base::take(*i));
|
||||
} else {
|
||||
now.push_back({ .id = entry.id });
|
||||
}
|
||||
apply(now.back(), entry);
|
||||
}
|
||||
if (now.empty()) {
|
||||
_count = 0;
|
||||
}
|
||||
for (auto &entry : _entries) {
|
||||
if (entry.subscribed) {
|
||||
entry.userpic->subscribeToUpdates(nullptr);
|
||||
entry.subscribed = false;
|
||||
}
|
||||
}
|
||||
_entries = std::move(now);
|
||||
_selected = restoreIndex(selectedId);
|
||||
_pressed = restoreIndex(pressedId);
|
||||
_contexted = restoreIndex(contextedId);
|
||||
updateScrollMax();
|
||||
unsubscribeUserpics();
|
||||
_count = int(_entries.size());
|
||||
update();
|
||||
}
|
||||
|
||||
void TopPeersStrip::apply(Entry &entry, const TopPeersEntry &data) {
|
||||
Expects(entry.id == data.id);
|
||||
Expects(data.userpic != nullptr);
|
||||
|
||||
if (entry.name.toString() != data.name) {
|
||||
entry.name.setText(st::topPeers.nameStyle, data.name);
|
||||
}
|
||||
if (entry.userpic.get() != data.userpic.get()) {
|
||||
if (entry.subscribed) {
|
||||
entry.userpic->subscribeToUpdates(nullptr);
|
||||
}
|
||||
entry.userpic = data.userpic;
|
||||
if (entry.subscribed) {
|
||||
subscribeUserpic(entry);
|
||||
}
|
||||
}
|
||||
if (entry.online != data.online) {
|
||||
entry.online = data.online;
|
||||
if (!entry.subscribed) {
|
||||
entry.onlineShown.stop();
|
||||
} else {
|
||||
entry.onlineShown.start(
|
||||
[=] { update(); },
|
||||
entry.online ? 0. : 1.,
|
||||
entry.online ? 1. : 0.,
|
||||
st::dialogsOnlineBadgeDuration);
|
||||
}
|
||||
}
|
||||
if (entry.badge != data.badge) {
|
||||
entry.badge = data.badge;
|
||||
entry.badgeString = QString();
|
||||
entry.userpicFrameDirty = 1;
|
||||
}
|
||||
if (entry.unread != data.unread) {
|
||||
entry.unread = data.unread;
|
||||
if (!entry.badge) {
|
||||
entry.userpicFrameDirty = 1;
|
||||
}
|
||||
}
|
||||
if (entry.muted != data.muted) {
|
||||
entry.muted = data.muted;
|
||||
if (entry.badge || entry.unread) {
|
||||
entry.userpicFrameDirty = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QRect TopPeersStrip::outer() const {
|
||||
const auto &st = st::topPeers;
|
||||
const auto single = st.photoLeft * 2 + st.photo;
|
||||
return QRect(0, 0, single, st::topPeers.height);
|
||||
}
|
||||
|
||||
QRect TopPeersStrip::innerRounded() const {
|
||||
return outer().marginsRemoved(st::topPeersMargin);
|
||||
}
|
||||
|
||||
int TopPeersStrip::scrollLeft() const {
|
||||
const auto value = _expandAnimation.value(_expanded.current() ? 1. : 0.);
|
||||
return anim::interpolate(_scrollLeft, 0, value);
|
||||
}
|
||||
|
||||
void TopPeersStrip::paintStrip(QRect clip) {
|
||||
auto p = Painter(&_strip);
|
||||
|
||||
const auto &st = st::topPeers;
|
||||
const auto scroll = scrollLeft();
|
||||
|
||||
const auto rows = (height() + st.height - 1) / st.height;
|
||||
const auto fromrow = std::min(clip.y() / st.height, rows);
|
||||
const auto tillrow = std::min(
|
||||
(clip.y() + clip.height() + st.height - 1) / st.height,
|
||||
rows);
|
||||
const auto layout = currentLayout();
|
||||
const auto fsingle = layout.fsingle;
|
||||
const auto added = layout.added;
|
||||
|
||||
for (auto row = fromrow; row != tillrow; ++row) {
|
||||
const auto shift = scroll + row * layout.inrow * fsingle;
|
||||
const auto from = std::min(
|
||||
int(std::floor((shift + clip.x()) / fsingle)),
|
||||
int(_entries.size()));
|
||||
const auto till = std::clamp(
|
||||
int(std::ceil(
|
||||
(shift + clip.x() + clip.width() + fsingle - 1) / fsingle + 1
|
||||
)),
|
||||
from,
|
||||
int(_entries.size()));
|
||||
|
||||
auto x = int(base::SafeRound(-shift + from * fsingle + added));
|
||||
auto y = row * st.height;
|
||||
const auto highlighted = (_contexted >= 0)
|
||||
? _contexted
|
||||
: (_pressed >= 0)
|
||||
? _pressed
|
||||
: _selected;
|
||||
for (auto i = from; i != till; ++i) {
|
||||
auto &entry = _entries[i];
|
||||
const auto selected = (i == highlighted);
|
||||
if (selected) {
|
||||
_selection.paint(p, innerRounded().translated(x, y));
|
||||
}
|
||||
if (entry.ripple) {
|
||||
entry.ripple->paint(
|
||||
p,
|
||||
x + st::topPeersMargin.left(),
|
||||
y + st::topPeersMargin.top(),
|
||||
width());
|
||||
if (entry.ripple->empty()) {
|
||||
entry.ripple = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if (!entry.subscribed) {
|
||||
subscribeUserpic(entry);
|
||||
}
|
||||
paintUserpic(p, x, y, i, selected);
|
||||
|
||||
p.setPen(st::dialogsNameFg);
|
||||
entry.name.drawElided(
|
||||
p,
|
||||
x + st.nameLeft,
|
||||
y + st.nameTop,
|
||||
layout.single - 2 * st.nameLeft,
|
||||
1,
|
||||
style::al_top);
|
||||
x += fsingle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TopPeersStrip::paintUserpic(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int index,
|
||||
bool selected) {
|
||||
Expects(index >= 0 && index < _entries.size());
|
||||
|
||||
auto &entry = _entries[index];
|
||||
const auto &st = st::topPeers;
|
||||
const auto size = st.photo;
|
||||
const auto rect = QRect(x + st.photoLeft, y + st.photoTop, size, size);
|
||||
|
||||
const auto online = entry.onlineShown.value(entry.online ? 1. : 0.);
|
||||
const auto useFrame = !entry.userpicFrame.isNull()
|
||||
&& !entry.userpicFrameDirty
|
||||
&& (entry.userpicFrameOnline == online);
|
||||
if (useFrame) {
|
||||
p.drawImage(rect, entry.userpicFrame);
|
||||
return;
|
||||
}
|
||||
const auto simple = entry.userpic->image(size);
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto renderFrame = (online > 0) || entry.badge || entry.unread;
|
||||
if (!renderFrame) {
|
||||
entry.userpicFrame = QImage();
|
||||
p.drawImage(rect, simple);
|
||||
return;
|
||||
} else if (entry.userpicFrame.size() != QSize(size, size) * ratio) {
|
||||
entry.userpicFrame = QImage(
|
||||
QSize(size, size) * ratio,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
entry.userpicFrame.setDevicePixelRatio(ratio);
|
||||
}
|
||||
entry.userpicFrame.fill(Qt::transparent);
|
||||
entry.userpicFrameDirty = 0;
|
||||
entry.userpicFrameOnline = online;
|
||||
|
||||
auto q = QPainter(&entry.userpicFrame);
|
||||
const auto inner = QRect(0, 0, size, size);
|
||||
q.drawImage(inner, simple);
|
||||
|
||||
auto hq = PainterHighQualityEnabler(q);
|
||||
|
||||
if (online > 0) {
|
||||
q.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
const auto onlineSize = st::dialogsOnlineBadgeSize;
|
||||
const auto stroke = st::dialogsOnlineBadgeStroke;
|
||||
const auto skip = st::dialogsOnlineBadgeSkip;
|
||||
const auto shrink = (onlineSize / 2) * (1. - online);
|
||||
|
||||
auto pen = QPen(Qt::transparent);
|
||||
pen.setWidthF(stroke * online);
|
||||
q.setPen(pen);
|
||||
q.setBrush(st::dialogsOnlineBadgeFg);
|
||||
q.drawEllipse(QRectF(
|
||||
size - skip.x() - onlineSize,
|
||||
size - skip.y() - onlineSize,
|
||||
onlineSize,
|
||||
onlineSize
|
||||
).marginsRemoved({ shrink, shrink, shrink, shrink }));
|
||||
q.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
}
|
||||
|
||||
if (entry.badge || entry.unread) {
|
||||
if (entry.badgeString.isEmpty()) {
|
||||
entry.badgeString = !entry.badge
|
||||
? u" "_q
|
||||
: (entry.badge < 1000)
|
||||
? QString::number(entry.badge)
|
||||
: (QString::number(entry.badge / 1000) + 'K');
|
||||
}
|
||||
auto st = Ui::UnreadBadgeStyle();
|
||||
st.selected = selected;
|
||||
st.muted = entry.muted;
|
||||
const auto &counter = entry.badgeString;
|
||||
const auto badge = PaintUnreadBadge(q, counter, size, 0, st);
|
||||
|
||||
const auto width = style::ConvertScaleExact(2.);
|
||||
const auto add = (width - style::ConvertScaleExact(1.)) / 2.;
|
||||
auto pen = QPen(Qt::transparent);
|
||||
pen.setWidthF(width);
|
||||
q.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
q.setPen(pen);
|
||||
q.setBrush(Qt::NoBrush);
|
||||
q.drawEllipse(QRectF(badge).marginsAdded({ add, add, add, add }));
|
||||
}
|
||||
|
||||
q.end();
|
||||
|
||||
p.drawImage(rect, entry.userpicFrame);
|
||||
}
|
||||
|
||||
void TopPeersStrip::stripContextMenuEvent(QContextMenuEvent *e) {
|
||||
_menu = nullptr;
|
||||
|
||||
if (e->reason() == QContextMenuEvent::Mouse) {
|
||||
_lastMousePosition = e->globalPos();
|
||||
_selectionByKeyboard = false;
|
||||
updateSelected();
|
||||
}
|
||||
if (_selected < 0 || _entries.empty()) {
|
||||
return;
|
||||
}
|
||||
Assert(_selected < _entries.size());
|
||||
_menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
this,
|
||||
st::popupMenuWithIcons);
|
||||
_showMenuRequests.fire({
|
||||
_entries[_selected].id,
|
||||
Ui::Menu::CreateAddActionCallback(_menu),
|
||||
});
|
||||
if (_menu->empty()) {
|
||||
_menu = nullptr;
|
||||
return;
|
||||
}
|
||||
const auto updateAfterMenuDestroyed = [=] {
|
||||
const auto globalPosition = QCursor::pos();
|
||||
if (rect().contains(mapFromGlobal(globalPosition))) {
|
||||
_lastMousePosition = globalPosition;
|
||||
_selectionByKeyboard = false;
|
||||
updateSelected();
|
||||
}
|
||||
};
|
||||
QObject::connect(
|
||||
_menu.get(),
|
||||
&QObject::destroyed,
|
||||
crl::guard(&_menuGuard, updateAfterMenuDestroyed));
|
||||
_menu->popup(e->globalPos());
|
||||
e->accept();
|
||||
}
|
||||
|
||||
bool TopPeersStrip::finishDragging() {
|
||||
if (!_dragging) {
|
||||
return false;
|
||||
}
|
||||
checkDragging();
|
||||
_dragging = false;
|
||||
_selectionByKeyboard = false;
|
||||
updateSelected();
|
||||
return true;
|
||||
}
|
||||
|
||||
TopPeersStrip::Layout TopPeersStrip::currentLayout() const {
|
||||
const auto single = outer().width();
|
||||
const auto inrow = std::max(width() / single, 1);
|
||||
const auto value = _expandAnimation.value(_expanded.current() ? 1. : 0.);
|
||||
const auto esingle = (width() / float64(inrow));
|
||||
const auto fsingle = single + (esingle - single) * value;
|
||||
|
||||
return {
|
||||
.single = single,
|
||||
.inrow = inrow,
|
||||
.fsingle = fsingle,
|
||||
.added = (fsingle - single) / 2.,
|
||||
};
|
||||
}
|
||||
|
||||
void TopPeersStrip::updateSelected() {
|
||||
if (_pressed >= 0 || !_lastMousePosition || _selectionByKeyboard) {
|
||||
return;
|
||||
}
|
||||
const auto p = _strip.mapFromGlobal(*_lastMousePosition);
|
||||
const auto expanded = _expanded.current();
|
||||
const auto row = expanded ? (p.y() / st::topPeers.height) : 0;
|
||||
const auto layout = currentLayout();
|
||||
const auto column = (_scrollLeft + p.x()) / layout.fsingle;
|
||||
const auto index = row * layout.inrow + int(std::floor(column));
|
||||
setSelected((index < 0 || index >= _entries.size()) ? -1 : index);
|
||||
}
|
||||
|
||||
void TopPeersStrip::setSelected(int selected) {
|
||||
if (_selected != selected) {
|
||||
const auto over = (selected >= 0);
|
||||
if (over != (_selected >= 0)) {
|
||||
setCursor(over ? style::cur_pointer : style::cur_default);
|
||||
}
|
||||
_selected = selected;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void TopPeersStrip::clearSelection() {
|
||||
setSelected(-1);
|
||||
}
|
||||
|
||||
void TopPeersStrip::scrollToSelected() {
|
||||
if (_selected < 0) {
|
||||
return;
|
||||
} else if (_expanded.current()) {
|
||||
const auto layout = currentLayout();
|
||||
const auto row = _selected / layout.inrow;
|
||||
const auto header = _header.height();
|
||||
const auto top = header + row * st::topPeers.height;
|
||||
const auto bottom = top + st::topPeers.height;
|
||||
_scrollToRequests.fire({ top - (row ? 0 : header), bottom});
|
||||
} else {
|
||||
const auto single = outer().width();
|
||||
const auto left = _selected * single;
|
||||
const auto right = left + single;
|
||||
if (_scrollLeft > left) {
|
||||
_scrollLeft = std::clamp(left, 0, _scrollLeftMax);
|
||||
} else if (_scrollLeft + width() < right) {
|
||||
_scrollLeft = std::clamp(right - width(), 0, _scrollLeftMax);
|
||||
}
|
||||
const auto height = _header.height() + st::topPeers.height;
|
||||
_scrollToRequests.fire({ 0, height });
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
||||
151
Telegram/SourceFiles/dialogs/ui/top_peers_strip.h
Normal file
151
Telegram/SourceFiles/dialogs/ui/top_peers_strip.h
Normal file
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/weak_ptr.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/widgets/menu/menu_add_action_callback.h"
|
||||
#include "ui/round_rect.h"
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
namespace Ui {
|
||||
class DynamicImage;
|
||||
class LinkButton;
|
||||
struct ScrollToRequest;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
struct TopPeersEntry {
|
||||
uint64 id = 0;
|
||||
QString name;
|
||||
std::shared_ptr<Ui::DynamicImage> userpic;
|
||||
uint32 badge : 28 = 0;
|
||||
uint32 unread : 1 = 0;
|
||||
uint32 muted : 1 = 0;
|
||||
uint32 online : 1 = 0;
|
||||
};
|
||||
|
||||
struct TopPeersList {
|
||||
std::vector<TopPeersEntry> entries;
|
||||
};
|
||||
|
||||
struct ShowTopPeerMenuRequest {
|
||||
uint64 id = 0;
|
||||
Ui::Menu::MenuCallback callback;
|
||||
};
|
||||
|
||||
class TopPeersStrip final : public Ui::RpWidget {
|
||||
public:
|
||||
TopPeersStrip(
|
||||
not_null<QWidget*> parent,
|
||||
rpl::producer<TopPeersList> content);
|
||||
~TopPeersStrip();
|
||||
|
||||
[[nodiscard]] bool empty() const;
|
||||
[[nodiscard]] rpl::producer<bool> emptyValue() const;
|
||||
[[nodiscard]] rpl::producer<uint64> clicks() const;
|
||||
[[nodiscard]] rpl::producer<uint64> pressed() const;
|
||||
[[nodiscard]] rpl::producer<> pressCancelled() const;
|
||||
[[nodiscard]] auto showMenuRequests() const
|
||||
-> rpl::producer<ShowTopPeerMenuRequest>;
|
||||
[[nodiscard]] auto scrollToRequests() const
|
||||
-> rpl::producer<Ui::ScrollToRequest>;
|
||||
|
||||
void removeLocally(uint64 id = 0);
|
||||
|
||||
[[nodiscard]] bool selectedByKeyboard() const;
|
||||
bool selectByKeyboard(Qt::Key direction);
|
||||
void deselectByKeyboard();
|
||||
bool chooseRow();
|
||||
void pressLeftToContextMenu(bool shown);
|
||||
|
||||
uint64 updateFromParentDrag(QPoint globalPosition);
|
||||
void dragLeft();
|
||||
|
||||
[[nodiscard]] auto verticalScrollEvents() const
|
||||
-> rpl::producer<not_null<QWheelEvent*>>;
|
||||
|
||||
private:
|
||||
struct Entry;
|
||||
struct Layout;
|
||||
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
|
||||
void setupHeader();
|
||||
void setupStrip();
|
||||
|
||||
void paintStrip(QRect clip);
|
||||
void stripWheelEvent(QWheelEvent *e);
|
||||
void stripMousePressEvent(QMouseEvent *e);
|
||||
void stripMouseMoveEvent(QMouseEvent *e);
|
||||
void stripMouseReleaseEvent(QMouseEvent *e);
|
||||
void stripContextMenuEvent(QContextMenuEvent *e);
|
||||
void stripLeaveEvent(QEvent *e);
|
||||
|
||||
void updateScrollMax(int newWidth = 0);
|
||||
void updateSelected();
|
||||
void setSelected(int selected);
|
||||
void setExpanded(bool expanded);
|
||||
void scrollToSelected();
|
||||
void checkDragging();
|
||||
bool finishDragging();
|
||||
void subscribeUserpic(Entry &entry);
|
||||
void unsubscribeUserpics(bool all = false);
|
||||
void paintUserpic(Painter &p, int x, int y, int index, bool selected);
|
||||
void clearSelection();
|
||||
void selectByMouse(QPoint globalPosition);
|
||||
|
||||
[[nodiscard]] QRect outer() const;
|
||||
[[nodiscard]] QRect innerRounded() const;
|
||||
[[nodiscard]] int scrollLeft() const;
|
||||
[[nodiscard]] Layout currentLayout() const;
|
||||
int clearPressed();
|
||||
void apply(const TopPeersList &list);
|
||||
void apply(Entry &entry, const TopPeersEntry &data);
|
||||
|
||||
Ui::RpWidget _header;
|
||||
Ui::RpWidget _strip;
|
||||
|
||||
std::vector<Entry> _entries;
|
||||
rpl::variable<int> _count = 0;
|
||||
base::flat_set<uint64> _removed;
|
||||
rpl::variable<Ui::LinkButton*> _toggleExpanded = nullptr;
|
||||
|
||||
rpl::event_stream<uint64> _clicks;
|
||||
rpl::event_stream<uint64> _presses;
|
||||
rpl::event_stream<> _pressCancelled;
|
||||
rpl::event_stream<ShowTopPeerMenuRequest> _showMenuRequests;
|
||||
rpl::event_stream<not_null<QWheelEvent*>> _verticalScrollEvents;
|
||||
|
||||
std::optional<QPoint> _lastMousePosition;
|
||||
std::optional<QPoint> _mouseDownPosition;
|
||||
int _startDraggingLeft = 0;
|
||||
int _scrollLeft = 0;
|
||||
int _scrollLeftMax = 0;
|
||||
bool _dragging = false;
|
||||
Qt::Orientation _scrollingLock = {};
|
||||
|
||||
int _selected = -1;
|
||||
int _pressed = -1;
|
||||
int _contexted = -1;
|
||||
bool _selectionByKeyboard = false;
|
||||
bool _hiddenLocally = false;
|
||||
|
||||
Ui::Animations::Simple _expandAnimation;
|
||||
rpl::variable<bool> _expanded = false;
|
||||
|
||||
rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
|
||||
|
||||
Ui::RoundRect _selection;
|
||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||
base::has_weak_ptr _menuGuard;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Dialogs
|
||||
Reference in New Issue
Block a user