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

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

View File

@@ -0,0 +1,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

View 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

View 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

View File

@@ -0,0 +1,107 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/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

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,47 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff

View 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

View File

@@ -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

View File

@@ -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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,151 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#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