mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-14 13:17:08 +02:00
Allow filtering Saved Messages search by tags.
This commit is contained in:
parent
e667436a98
commit
9c151ca151
10 changed files with 518 additions and 36 deletions
|
@ -609,6 +609,8 @@ PRIVATE
|
|||
dialogs/dialogs_row.h
|
||||
dialogs/dialogs_search_from_controllers.cpp
|
||||
dialogs/dialogs_search_from_controllers.h
|
||||
dialogs/dialogs_search_tags.cpp
|
||||
dialogs/dialogs_search_tags.h
|
||||
dialogs/dialogs_widget.cpp
|
||||
dialogs/dialogs_widget.h
|
||||
dialogs/ui/dialogs_layout.cpp
|
||||
|
|
|
@ -623,3 +623,6 @@ dialogsStoriesTooltipHide: IconButton(defaultIconButton) {
|
|||
searchedBarHeight: 32px;
|
||||
searchedBarFont: normalFont;
|
||||
searchedBarPosition: point(17px, 7px);
|
||||
|
||||
dialogsSearchTagSkip: point(8px, 4px);
|
||||
dialogsSearchTagBottom: 10px;
|
||||
|
|
|
@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "dialogs/dialogs_indexed_list.h"
|
||||
#include "dialogs/dialogs_widget.h"
|
||||
#include "dialogs/dialogs_search_from_controllers.h"
|
||||
#include "dialogs/dialogs_search_tags.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "core/shortcuts.h"
|
||||
|
@ -40,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "data/data_chat_filters.h"
|
||||
#include "data/data_cloud_file.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_message_reactions.h"
|
||||
#include "data/data_saved_messages.h"
|
||||
#include "data/data_stories.h"
|
||||
#include "data/stickers/data_stickers.h"
|
||||
|
@ -477,18 +479,26 @@ int InnerWidget::peerSearchOffset() const {
|
|||
+ st::searchedBarHeight;
|
||||
}
|
||||
|
||||
int InnerWidget::searchedOffset() const {
|
||||
auto result = peerSearchOffset();
|
||||
int InnerWidget::searchInChatOffset() const {
|
||||
auto result = peerSearchOffset() - st::searchedBarHeight;
|
||||
if (!_peerSearchResults.empty()) {
|
||||
result += (_peerSearchResults.size() * st::dialogsRowHeight)
|
||||
+ st::searchedBarHeight;
|
||||
}
|
||||
result += searchInChatSkip();
|
||||
return result;
|
||||
}
|
||||
|
||||
int InnerWidget::searchedOffset() const {
|
||||
return searchInChatOffset()
|
||||
+ searchInChatSkip()
|
||||
+ st::searchedBarHeight;
|
||||
}
|
||||
|
||||
int InnerWidget::searchInChatSkip() const {
|
||||
auto result = 0;
|
||||
if (_searchTags) {
|
||||
result += _searchTags->height();
|
||||
}
|
||||
if (_searchInChat) {
|
||||
result += st::searchedBarHeight + st::dialogsSearchInHeight;
|
||||
}
|
||||
|
@ -1111,12 +1121,20 @@ void InnerWidget::paintSearchInChat(
|
|||
auto height = searchInChatSkip();
|
||||
|
||||
auto top = 0;
|
||||
if (_searchTags) {
|
||||
const auto height = _searchTags->height();
|
||||
p.fillRect(0, top, width(), height, currentBg());
|
||||
const auto position = QPoint(_searchTagsLeft, 0);
|
||||
_searchTags->paint(p, position, context.now, context.paused);
|
||||
top += height;
|
||||
}
|
||||
p.setFont(st::searchedBarFont);
|
||||
if (_searchInChat) {
|
||||
top += st::searchedBarHeight;
|
||||
p.fillRect(0, 0, width(), top, st::searchedBarBg);
|
||||
const auto bar = st::searchedBarHeight;
|
||||
p.fillRect(0, top, width(), top + bar, st::searchedBarBg);
|
||||
p.setPen(st::searchedBarFg);
|
||||
p.drawTextLeft(st::searchedBarPosition.x(), st::searchedBarPosition.y(), width(), tr::lng_dlg_search_in(tr::now));
|
||||
p.drawTextLeft(st::searchedBarPosition.x(), top + st::searchedBarPosition.y(), width(), tr::lng_dlg_search_in(tr::now));
|
||||
top += bar;
|
||||
}
|
||||
auto fullRect = QRect(0, top, width(), height - top);
|
||||
p.fillRect(fullRect, currentBg());
|
||||
|
@ -1276,6 +1294,21 @@ void InnerWidget::selectByMouse(QPoint globalPosition) {
|
|||
_lastMousePosition = globalPosition;
|
||||
_lastRowLocalMouseX = local.x();
|
||||
|
||||
const auto tagBase = QPoint(_searchTagsLeft, searchInChatOffset());
|
||||
const auto tagPoint = local - tagBase;
|
||||
const auto inTags = _searchTags
|
||||
&& QRect(
|
||||
tagBase,
|
||||
QSize(width() - 2 * _searchTagsLeft, _searchTags->height())
|
||||
).contains(local);
|
||||
const auto tagLink = inTags
|
||||
? _searchTags->lookupHandler(tagPoint)
|
||||
: nullptr;
|
||||
ClickHandler::setActive(tagLink);
|
||||
if (inTags) {
|
||||
setCursor(tagLink ? style::cur_pointer : style::cur_default);
|
||||
}
|
||||
|
||||
const auto w = width();
|
||||
const auto mouseY = local.y();
|
||||
clearIrrelevantState();
|
||||
|
@ -1370,7 +1403,7 @@ void InnerWidget::selectByMouse(QPoint globalPosition) {
|
|||
updateSelectedRow();
|
||||
}
|
||||
}
|
||||
if (wasSelected != isSelected()) {
|
||||
if (!inTags && wasSelected != isSelected()) {
|
||||
setCursor(wasSelected ? style::cur_default : style::cur_pointer);
|
||||
}
|
||||
}
|
||||
|
@ -1452,6 +1485,7 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) {
|
|||
QSize(width(), _st->height),
|
||||
row->repaint());
|
||||
}
|
||||
ClickHandler::pressed();
|
||||
if (anim::Disabled()
|
||||
&& (!_pressed || !_pressed->entry()->isPinnedDialog(_filterId))) {
|
||||
mousePressReleased(e->globalPos(), e->button(), e->modifiers());
|
||||
|
@ -1743,6 +1777,9 @@ void InnerWidget::mousePressReleased(
|
|||
chooseRow(modifiers, pressedTopicRootId);
|
||||
}
|
||||
}
|
||||
if (auto activated = ClickHandler::unpressed()) {
|
||||
ActivateClickHandler(window(), activated, { button });
|
||||
}
|
||||
}
|
||||
|
||||
void InnerWidget::setCollapsedPressed(int pressed) {
|
||||
|
@ -1825,9 +1862,10 @@ void InnerWidget::moveCancelSearchButtons() {
|
|||
st::columnMinimalWidthLeft - _narrowWidth);
|
||||
const auto left = widthForCancelButton - st::dialogsSearchInSkip - _cancelSearchInChat->width();
|
||||
const auto top = (st::dialogsSearchInHeight - st::dialogsCancelSearchInPeer.height) / 2;
|
||||
_cancelSearchInChat->moveToLeft(left, st::searchedBarHeight + top);
|
||||
const auto skip = _searchInChat ? (st::searchedBarHeight + st::dialogsSearchInHeight + st::lineWidth) : 0;
|
||||
_cancelSearchFromUser->moveToLeft(left, skip + top);
|
||||
const auto skip = st::searchedBarHeight + (_searchTags ? _searchTags->height() : 0);
|
||||
_cancelSearchInChat->moveToLeft(left, skip + top);
|
||||
const auto next = _searchInChat ? (skip + st::dialogsSearchInHeight + st::lineWidth) : 0;
|
||||
_cancelSearchFromUser->moveToLeft(left, next + top);
|
||||
}
|
||||
|
||||
void InnerWidget::dialogRowReplaced(
|
||||
|
@ -2330,7 +2368,9 @@ void InnerWidget::applyFilterUpdate(QString newFilter, bool force) {
|
|||
newFilter = words.isEmpty() ? QString() : words.join(' ');
|
||||
if (newFilter != _filter || force) {
|
||||
_filter = newFilter;
|
||||
if (_filter.isEmpty() && !_searchFromPeer) {
|
||||
if (_filter.isEmpty()
|
||||
&& !_searchFromPeer
|
||||
&& _searchTagsSelected.empty()) {
|
||||
clearFilter();
|
||||
} else {
|
||||
setState(WidgetState::Filtered);
|
||||
|
@ -2350,7 +2390,9 @@ void InnerWidget::applyFilterUpdate(QString newFilter, bool force) {
|
|||
top += i->row->height();
|
||||
}
|
||||
};
|
||||
if (!_searchInChat && !_searchFromPeer && !words.isEmpty()) {
|
||||
if (!_searchInChat
|
||||
&& !_searchFromPeer
|
||||
&& !words.isEmpty()) {
|
||||
if (_savedSublists) {
|
||||
const auto owner = &session().data();
|
||||
append(owner->savedMessages().chatsList()->indexed());
|
||||
|
@ -2791,6 +2833,11 @@ void InnerWidget::refresh(bool toTop) {
|
|||
return refreshWithCollapsedRows(toTop);
|
||||
}
|
||||
refreshEmptyLabel();
|
||||
if (_searchTags) {
|
||||
_searchTagsLeft = st::dialogsFilterSkip
|
||||
+ st::dialogsFilterPadding.x();
|
||||
_searchTags->resizeToWidth(width() - 2 * _searchTagsLeft);
|
||||
}
|
||||
auto h = 0;
|
||||
if (_state == WidgetState::Default) {
|
||||
if (_shownList->empty()) {
|
||||
|
@ -2926,6 +2973,43 @@ void InnerWidget::searchInChat(Key key, PeerData *from) {
|
|||
} else if (const auto migrateFrom = peer->migrateFrom()) {
|
||||
_searchInMigrated = peer->owner().history(migrateFrom);
|
||||
}
|
||||
|
||||
if (peer->isSelf()) {
|
||||
const auto reactions = &peer->owner().reactions();
|
||||
const auto list = [=] {
|
||||
return reactions->list(Data::Reactions::Type::MyTags);
|
||||
};
|
||||
_searchTags = std::make_unique<SearchTags>(
|
||||
&peer->owner(),
|
||||
rpl::single(
|
||||
list()
|
||||
) | rpl::then(
|
||||
reactions->myTagsUpdates() | rpl::map(list)
|
||||
));
|
||||
|
||||
_searchTags->selectedValue(
|
||||
) | rpl::start_with_next([=](std::vector<Data::ReactionId> &&list) {
|
||||
_searchTagsSelected = std::move(list);
|
||||
}, _searchTags->lifetime());
|
||||
|
||||
_searchTags->repaintRequests() | rpl::start_with_next([=] {
|
||||
const auto height = _searchTags->height();
|
||||
update(0, searchInChatOffset(), width(), height);
|
||||
}, _searchTags->lifetime());
|
||||
|
||||
_searchTags->heightValue() | rpl::filter(
|
||||
rpl::mappers::_1 > 0
|
||||
) | rpl::start_with_next([=] {
|
||||
refresh();
|
||||
moveCancelSearchButtons();
|
||||
}, _searchTags->lifetime());
|
||||
} else {
|
||||
_searchTags = nullptr;
|
||||
_searchTagsSelected.clear();
|
||||
}
|
||||
} else {
|
||||
_searchTags = nullptr;
|
||||
_searchTagsSelected.clear();
|
||||
}
|
||||
_searchInChat = key;
|
||||
_searchFromPeer = from;
|
||||
|
@ -2957,6 +3041,13 @@ void InnerWidget::searchInChat(Key key, PeerData *from) {
|
|||
_searchInChat || !_filter.isEmpty());
|
||||
}
|
||||
|
||||
auto InnerWidget::searchTagsValue() const
|
||||
-> rpl::producer<std::vector<Data::ReactionId>> {
|
||||
return _searchTags
|
||||
? _searchTags->selectedValue()
|
||||
: rpl::single(std::vector<Data::ReactionId>());
|
||||
}
|
||||
|
||||
void InnerWidget::refreshSearchInChatLabel() {
|
||||
const auto dialog = [&] {
|
||||
if (const auto topic = _searchInChat.topic()) {
|
||||
|
|
|
@ -43,6 +43,7 @@ namespace Data {
|
|||
class Thread;
|
||||
class Folder;
|
||||
class Forum;
|
||||
struct ReactionId;
|
||||
} // namespace Data
|
||||
|
||||
namespace Dialogs::Ui {
|
||||
|
@ -57,6 +58,7 @@ namespace Dialogs {
|
|||
class Row;
|
||||
class FakeRow;
|
||||
class IndexedList;
|
||||
class SearchTags;
|
||||
|
||||
struct ChosenRow {
|
||||
Key key;
|
||||
|
@ -138,6 +140,8 @@ public:
|
|||
[[nodiscard]] bool hasFilteredResults() const;
|
||||
|
||||
void searchInChat(Key key, PeerData *from);
|
||||
[[nodiscard]] auto searchTagsValue() const
|
||||
-> rpl::producer<std::vector<Data::ReactionId>>;
|
||||
|
||||
void applyFilterUpdate(QString newFilter, bool force = false);
|
||||
void onHashtagFilterUpdate(QStringView newFilter);
|
||||
|
@ -325,6 +329,7 @@ private:
|
|||
[[nodiscard]] int filteredIndex(int y) const;
|
||||
[[nodiscard]] int filteredHeight(int till = -1) const;
|
||||
[[nodiscard]] int peerSearchOffset() const;
|
||||
[[nodiscard]] int searchInChatOffset() const;
|
||||
[[nodiscard]] int searchedOffset() const;
|
||||
[[nodiscard]] int searchInChatSkip() const;
|
||||
|
||||
|
@ -482,6 +487,9 @@ private:
|
|||
mutable Ui::PeerUserpicView _searchFromUserUserpic;
|
||||
Ui::Text::String _searchInChatText;
|
||||
Ui::Text::String _searchFromUserText;
|
||||
std::unique_ptr<SearchTags> _searchTags;
|
||||
std::vector<Data::ReactionId> _searchTagsSelected;
|
||||
int _searchTagsLeft = 0;
|
||||
RowDescriptor _menuRow;
|
||||
|
||||
base::flat_map<
|
||||
|
|
246
Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp
Normal file
246
Telegram/SourceFiles/dialogs/dialogs_search_tags.cpp
Normal file
|
@ -0,0 +1,246 @@
|
|||
/*
|
||||
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/dialogs_search_tags.h"
|
||||
|
||||
#include "data/stickers/data_custom_emoji.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_message_reactions.h"
|
||||
#include "data/data_session.h"
|
||||
#include "history/view/reactions/history_view_reactions.h"
|
||||
#include "ui/effects/animation_value.h"
|
||||
#include "ui/power_saving.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
struct SearchTags::Tag {
|
||||
Data::ReactionId id;
|
||||
std::unique_ptr<Ui::Text::CustomEmoji> custom;
|
||||
mutable QImage image;
|
||||
QRect geometry;
|
||||
ClickHandlerPtr link;
|
||||
bool selected = false;
|
||||
};
|
||||
|
||||
SearchTags::SearchTags(
|
||||
not_null<Data::Session*> owner,
|
||||
rpl::producer<std::vector<Data::Reaction>> tags)
|
||||
: _owner(owner) {
|
||||
std::move(
|
||||
tags
|
||||
) | rpl::start_with_next([=](const std::vector<Data::Reaction> &list) {
|
||||
fill(list);
|
||||
}, _lifetime);
|
||||
|
||||
style::PaletteChanged(
|
||||
) | rpl::start_with_next([=] {
|
||||
_normalBg = _selectedBg = QImage();
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
SearchTags::~SearchTags() = default;
|
||||
|
||||
void SearchTags::fill(const std::vector<Data::Reaction> &list) {
|
||||
const auto selected = collectSelected();
|
||||
_tags.clear();
|
||||
_tags.reserve(list.size());
|
||||
const auto link = [&](Data::ReactionId id) {
|
||||
return std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
|
||||
const auto i = ranges::find(_tags, id, &Tag::id);
|
||||
if (i != end(_tags)) {
|
||||
i->selected = !i->selected;
|
||||
_selectedChanges.fire({});
|
||||
}
|
||||
}));
|
||||
};
|
||||
for (const auto &reaction : list) {
|
||||
const auto id = reaction.id;
|
||||
const auto customId = id.custom();
|
||||
_tags.push_back({
|
||||
.id = id,
|
||||
.custom = (customId
|
||||
? _owner->customEmojiManager().create(
|
||||
customId,
|
||||
[=] { _repaintRequests.fire({}); })
|
||||
: nullptr),
|
||||
.link = link(id),
|
||||
.selected = ranges::contains(selected, id),
|
||||
});
|
||||
if (!customId) {
|
||||
_owner->reactions().preloadImageFor(id);
|
||||
}
|
||||
}
|
||||
if (_width > 0) {
|
||||
layout();
|
||||
}
|
||||
}
|
||||
|
||||
void SearchTags::layout() {
|
||||
Expects(_width > 0);
|
||||
|
||||
const auto &bg = validateBg(false);
|
||||
const auto skip = st::dialogsSearchTagSkip;
|
||||
const auto size = bg.size() / bg.devicePixelRatio();
|
||||
const auto xsingle = size.width() + skip.x();
|
||||
const auto ysingle = size.height() + skip.y();
|
||||
const auto columns = std::max((_width + skip.x()) / xsingle, 1);
|
||||
const auto rows = (_tags.size() + columns - 1) / columns;
|
||||
for (auto row = 0; row != rows; ++row) {
|
||||
for (auto column = 0; column != columns; ++column) {
|
||||
const auto index = row * columns + column;
|
||||
if (index >= _tags.size()) {
|
||||
break;
|
||||
}
|
||||
const auto x = column * xsingle;
|
||||
const auto y = row * ysingle;
|
||||
_tags[index].geometry = QRect(QPoint(x, y), size);
|
||||
}
|
||||
}
|
||||
const auto bottom = st::dialogsSearchTagBottom;
|
||||
_height = rows ? (rows * ysingle - skip.y() + bottom) : 0;
|
||||
}
|
||||
|
||||
void SearchTags::resizeToWidth(int width) {
|
||||
if (_width == width || width <= 0) {
|
||||
return;
|
||||
}
|
||||
_width = width;
|
||||
layout();
|
||||
}
|
||||
|
||||
int SearchTags::height() const {
|
||||
return _height.current();
|
||||
}
|
||||
|
||||
rpl::producer<int> SearchTags::heightValue() const {
|
||||
return _height.value();
|
||||
}
|
||||
|
||||
rpl::producer<> SearchTags::repaintRequests() const {
|
||||
return _repaintRequests.events();
|
||||
}
|
||||
|
||||
ClickHandlerPtr SearchTags::lookupHandler(QPoint point) const {
|
||||
for (const auto &tag : _tags) {
|
||||
if (tag.geometry.contains(point.x(), point.y())) {
|
||||
return tag.link;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto SearchTags::selectedValue() const
|
||||
-> rpl::producer<std::vector<Data::ReactionId>> {
|
||||
return _selectedChanges.events() | rpl::map([=] {
|
||||
return collectSelected();
|
||||
});
|
||||
}
|
||||
|
||||
void SearchTags::paintCustomFrame(
|
||||
QPainter &p,
|
||||
not_null<Ui::Text::CustomEmoji*> emoji,
|
||||
QPoint innerTopLeft,
|
||||
crl::time now,
|
||||
bool paused,
|
||||
const QColor &textColor) const {
|
||||
if (_customCache.isNull()) {
|
||||
using namespace Ui::Text;
|
||||
const auto size = st::emojiSize;
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
const auto adjusted = AdjustCustomEmojiSize(size);
|
||||
_customCache = QImage(
|
||||
QSize(adjusted, adjusted) * factor,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
_customCache.setDevicePixelRatio(factor);
|
||||
_customSkip = (size - adjusted) / 2;
|
||||
}
|
||||
_customCache.fill(Qt::transparent);
|
||||
auto q = QPainter(&_customCache);
|
||||
emoji->paint(q, {
|
||||
.textColor = textColor,
|
||||
.now = now,
|
||||
.paused = paused || On(PowerSaving::kEmojiChat),
|
||||
});
|
||||
q.end();
|
||||
_customCache = Images::Round(
|
||||
std::move(_customCache),
|
||||
(Images::Option::RoundLarge
|
||||
| Images::Option::RoundSkipTopRight
|
||||
| Images::Option::RoundSkipBottomRight));
|
||||
|
||||
p.drawImage(
|
||||
innerTopLeft + QPoint(_customSkip, _customSkip),
|
||||
_customCache);
|
||||
}
|
||||
|
||||
void SearchTags::paint(
|
||||
QPainter &p,
|
||||
QPoint position,
|
||||
crl::time now,
|
||||
bool paused) const {
|
||||
const auto size = st::reactionInlineSize;
|
||||
const auto skip = (size - st::reactionInlineImage) / 2;
|
||||
const auto padding = st::reactionInlinePadding;
|
||||
for (const auto &tag : _tags) {
|
||||
const auto geometry = tag.geometry.translated(position);
|
||||
p.drawImage(geometry.topLeft(), validateBg(tag.selected));
|
||||
if (!tag.custom && tag.image.isNull()) {
|
||||
tag.image = _owner->reactions().resolveImageFor(
|
||||
tag.id,
|
||||
::Data::Reactions::ImageSize::InlineList);
|
||||
}
|
||||
const auto inner = geometry.marginsRemoved(padding);
|
||||
const auto image = QRect(
|
||||
inner.topLeft() + QPoint(skip, skip),
|
||||
QSize(st::reactionInlineImage, st::reactionInlineImage));
|
||||
if (const auto custom = tag.custom.get()) {
|
||||
const auto textFg = tag.selected
|
||||
? st::dialogsNameFgActive->c
|
||||
: st::dialogsNameFgOver->c;
|
||||
paintCustomFrame(
|
||||
p,
|
||||
custom,
|
||||
inner.topLeft(),
|
||||
now,
|
||||
paused,
|
||||
textFg);
|
||||
} else if (!tag.image.isNull()) {
|
||||
p.drawImage(image.topLeft(), tag.image);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const QImage &SearchTags::validateBg(bool selected) const {
|
||||
using namespace HistoryView::Reactions;
|
||||
auto &image = selected ? _selectedBg : _normalBg;
|
||||
if (image.isNull()) {
|
||||
const auto tagBg = selected
|
||||
? st::dialogsBgActive->c
|
||||
: st::dialogsBgOver->c;
|
||||
const auto dotBg = selected
|
||||
? anim::with_alpha(tagBg, InlineList::TagDotAlpha())
|
||||
: st::windowSubTextFg->c;
|
||||
image = InlineList::PrepareTagBg(tagBg, dotBg);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
std::vector<Data::ReactionId> SearchTags::collectSelected() const {
|
||||
return _tags | ranges::views::filter(
|
||||
&Tag::selected
|
||||
) | ranges::views::transform(
|
||||
&Tag::id
|
||||
) | ranges::to_vector;
|
||||
}
|
||||
|
||||
rpl::lifetime &SearchTags::lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
} // namespace Dialogs
|
78
Telegram/SourceFiles/dialogs/dialogs_search_tags.h
Normal file
78
Telegram/SourceFiles/dialogs/dialogs_search_tags.h
Normal file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/weak_ptr.h"
|
||||
|
||||
namespace Data {
|
||||
class Session;
|
||||
struct Reaction;
|
||||
struct ReactionId;
|
||||
} // namespace Data
|
||||
|
||||
namespace Ui::Text {
|
||||
class CustomEmoji;
|
||||
} // namespace Ui::Text
|
||||
|
||||
namespace Dialogs {
|
||||
|
||||
class SearchTags final : public base::has_weak_ptr {
|
||||
public:
|
||||
SearchTags(
|
||||
not_null<Data::Session*> owner,
|
||||
rpl::producer<std::vector<Data::Reaction>> tags);
|
||||
~SearchTags();
|
||||
|
||||
void resizeToWidth(int width);
|
||||
[[nodiscard]] int height() const;
|
||||
[[nodiscard]] rpl::producer<int> heightValue() const;
|
||||
[[nodiscard]] rpl::producer<> repaintRequests() const;
|
||||
|
||||
[[nodiscard]] ClickHandlerPtr lookupHandler(QPoint point) const;
|
||||
[[nodiscard]] auto selectedValue() const
|
||||
-> rpl::producer<std::vector<Data::ReactionId>>;
|
||||
|
||||
void paint(
|
||||
QPainter &p,
|
||||
QPoint position,
|
||||
crl::time now,
|
||||
bool paused) const;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
private:
|
||||
struct Tag;
|
||||
|
||||
void fill(const std::vector<Data::Reaction> &list);
|
||||
void paintCustomFrame(
|
||||
QPainter &p,
|
||||
not_null<Ui::Text::CustomEmoji*> emoji,
|
||||
QPoint innerTopLeft,
|
||||
crl::time now,
|
||||
bool paused,
|
||||
const QColor &textColor) const;
|
||||
void layout();
|
||||
[[nodiscard]] std::vector<Data::ReactionId> collectSelected() const;
|
||||
[[nodiscard]] const QImage &validateBg(bool selected) const;
|
||||
|
||||
const not_null<Data::Session*> _owner;
|
||||
std::vector<Tag> _tags;
|
||||
rpl::event_stream<> _selectedChanges;
|
||||
rpl::event_stream<> _repaintRequests;
|
||||
mutable QImage _normalBg;
|
||||
mutable QImage _selectedBg;
|
||||
mutable QImage _customCache;
|
||||
mutable int _customSkip = 0;
|
||||
rpl::variable<int> _height;
|
||||
int _width = 0;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Dialogs
|
|
@ -1713,7 +1713,7 @@ void Widget::loadMoreBlockedByDate() {
|
|||
bool Widget::searchMessages(bool searchCache) {
|
||||
auto result = false;
|
||||
auto q = currentSearchQuery().trimmed();
|
||||
if (q.isEmpty() && !_searchFromAuthor) {
|
||||
if (q.isEmpty() && !_searchFromAuthor && _searchTags.empty()) {
|
||||
cancelSearchRequest();
|
||||
_api.request(base::take(_peerSearchRequest)).cancel();
|
||||
_api.request(base::take(_topicSearchRequest)).cancel();
|
||||
|
@ -1730,6 +1730,7 @@ bool Widget::searchMessages(bool searchCache) {
|
|||
if (i != _searchCache.end()) {
|
||||
_searchQuery = q;
|
||||
_searchQueryFrom = _searchFromAuthor;
|
||||
_searchQueryTags = _searchTags;
|
||||
_searchNextRate = 0;
|
||||
_searchFull = _searchFullMigrated = false;
|
||||
cancelSearchRequest();
|
||||
|
@ -1741,9 +1742,12 @@ bool Widget::searchMessages(bool searchCache) {
|
|||
0);
|
||||
result = true;
|
||||
}
|
||||
} else if (_searchQuery != q || _searchQueryFrom != _searchFromAuthor) {
|
||||
} else if (_searchQuery != q
|
||||
|| _searchQueryFrom != _searchFromAuthor
|
||||
|| _searchQueryTags != _searchTags) {
|
||||
_searchQuery = q;
|
||||
_searchQueryFrom = _searchFromAuthor;
|
||||
_searchQueryTags = _searchTags;
|
||||
_searchNextRate = 0;
|
||||
_searchFull = _searchFullMigrated = false;
|
||||
cancelSearchRequest();
|
||||
|
@ -1757,14 +1761,20 @@ bool Widget::searchMessages(bool searchCache) {
|
|||
using Flag = MTPmessages_Search::Flag;
|
||||
_searchRequest = session().api().request(MTPmessages_Search(
|
||||
MTP_flags((topic ? Flag::f_top_msg_id : Flag())
|
||||
| (_searchQueryFrom ? Flag::f_from_id : Flag())),
|
||||
| (_searchQueryFrom ? Flag::f_from_id : Flag())
|
||||
| (_searchQueryTags.empty()
|
||||
? Flag()
|
||||
: Flag::f_saved_reaction)),
|
||||
peer->input,
|
||||
MTP_string(_searchQuery),
|
||||
(_searchQueryFrom
|
||||
? _searchQueryFrom->input
|
||||
: MTP_inputPeerEmpty()),
|
||||
MTPInputPeer(), // saved_peer_id
|
||||
MTPVector<MTPReaction>(), // saved_reaction
|
||||
MTP_vector_from_range(
|
||||
_searchQueryTags | ranges::views::transform(
|
||||
Data::ReactionToMTP
|
||||
)),
|
||||
MTP_int(topic ? topic->rootId() : 0),
|
||||
MTP_inputMessagesFilterEmpty(),
|
||||
MTP_int(0), // min_date
|
||||
|
@ -1868,6 +1878,7 @@ bool Widget::searchMessages(bool searchCache) {
|
|||
bool Widget::searchForPeersRequired(const QString &query) const {
|
||||
return !_searchInChat
|
||||
&& !_searchFromAuthor
|
||||
&& _searchTags.empty()
|
||||
&& !_openedForum
|
||||
&& !query.isEmpty()
|
||||
&& (query[0] != '#');
|
||||
|
@ -1876,6 +1887,7 @@ bool Widget::searchForPeersRequired(const QString &query) const {
|
|||
bool Widget::searchForTopicsRequired(const QString &query) const {
|
||||
return !_searchInChat
|
||||
&& !_searchFromAuthor
|
||||
&& _searchTags.empty()
|
||||
&& _openedForum
|
||||
&& !query.isEmpty()
|
||||
&& (query[0] != '#')
|
||||
|
@ -2000,14 +2012,20 @@ void Widget::searchMore() {
|
|||
using Flag = MTPmessages_Search::Flag;
|
||||
_searchRequest = session().api().request(MTPmessages_Search(
|
||||
MTP_flags((topic ? Flag::f_top_msg_id : Flag())
|
||||
| (_searchQueryFrom ? Flag::f_from_id : Flag())),
|
||||
| (_searchQueryFrom ? Flag::f_from_id : Flag())
|
||||
| (_searchQueryTags.empty()
|
||||
? Flag()
|
||||
: Flag::f_saved_reaction)),
|
||||
peer->input,
|
||||
MTP_string(_searchQuery),
|
||||
(_searchQueryFrom
|
||||
? _searchQueryFrom->input
|
||||
: MTP_inputPeerEmpty()),
|
||||
MTPInputPeer(), // saved_peer_id
|
||||
MTPVector<MTPReaction>(), // saved_reaction
|
||||
MTP_vector_from_range(
|
||||
_searchQueryTags | ranges::views::transform(
|
||||
Data::ReactionToMTP
|
||||
)),
|
||||
MTP_int(topic ? topic->rootId() : 0),
|
||||
MTP_inputMessagesFilterEmpty(),
|
||||
MTP_int(0), // min_date
|
||||
|
@ -2401,7 +2419,7 @@ void Widget::applyFilterUpdate(bool force) {
|
|||
updateStoriesVisibility();
|
||||
const auto filterText = currentSearchQuery();
|
||||
_inner->applyFilterUpdate(filterText, force);
|
||||
if (filterText.isEmpty() && !_searchFromAuthor) {
|
||||
if (filterText.isEmpty() && !_searchFromAuthor && _searchTags.empty()) {
|
||||
clearSearchCache();
|
||||
}
|
||||
_cancelSearch->toggle(!filterText.isEmpty(), anim::type::normal);
|
||||
|
@ -2417,7 +2435,9 @@ void Widget::applyFilterUpdate(bool force) {
|
|||
_peerSearchQuery = QString();
|
||||
}
|
||||
|
||||
if (_chooseFromUser->toggled() || _searchFromAuthor) {
|
||||
if (_chooseFromUser->toggled()
|
||||
|| _searchFromAuthor
|
||||
|| !_searchTags.empty()) {
|
||||
auto switchToChooseFrom = HistoryView::SwitchToChooseFromQuery();
|
||||
if (_lastFilterText != switchToChooseFrom
|
||||
&& switchToChooseFrom.startsWith(_lastFilterText)
|
||||
|
@ -2619,6 +2639,18 @@ bool Widget::setSearchInChat(Key chat, PeerData *from) {
|
|||
controller()->closeFolder();
|
||||
}
|
||||
_inner->searchInChat(_searchInChat, _searchFromAuthor);
|
||||
_searchTagsLifetime = _inner->searchTagsValue(
|
||||
) | rpl::start_with_next([=](std::vector<Data::ReactionId> &&list) {
|
||||
if (_searchTags != list) {
|
||||
clearSearchCache();
|
||||
_searchTags = std::move(list);
|
||||
if (_searchTags.empty()) {
|
||||
applyFilterUpdate(true);
|
||||
} else {
|
||||
searchMessages();
|
||||
}
|
||||
}
|
||||
});
|
||||
if (_subsectionTopBar) {
|
||||
_subsectionTopBar->searchEnableJumpToDate(
|
||||
_openedForum && _searchInChat);
|
||||
|
@ -2639,6 +2671,7 @@ void Widget::clearSearchCache() {
|
|||
}
|
||||
_searchQuery = QString();
|
||||
_searchQueryFrom = nullptr;
|
||||
_searchQueryTags.clear();
|
||||
_topicSearchQuery = QString();
|
||||
_topicSearchOffsetDate = 0;
|
||||
_topicSearchOffsetId = _topicSearchOffsetTopicId = 0;
|
||||
|
|
|
@ -22,6 +22,7 @@ class Error;
|
|||
namespace Data {
|
||||
class Forum;
|
||||
enum class StorySourcesList : uchar;
|
||||
struct ReactionId;
|
||||
} // namespace Data
|
||||
|
||||
namespace Main {
|
||||
|
@ -285,6 +286,8 @@ private:
|
|||
Dialogs::Key _searchInChat;
|
||||
History *_searchInMigrated = nullptr;
|
||||
PeerData *_searchFromAuthor = nullptr;
|
||||
std::vector<Data::ReactionId> _searchTags;
|
||||
rpl::lifetime _searchTagsLifetime;
|
||||
QString _lastFilterText;
|
||||
|
||||
rpl::event_stream<rpl::producer<Stories::Content>> _storiesContents;
|
||||
|
@ -313,6 +316,7 @@ private:
|
|||
|
||||
QString _searchQuery;
|
||||
PeerData *_searchQueryFrom = nullptr;
|
||||
std::vector<Data::ReactionId> _searchQueryTags;
|
||||
int32 _searchNextRate = 0;
|
||||
bool _searchFull = false;
|
||||
bool _searchFullMigrated = false;
|
||||
|
|
|
@ -507,12 +507,11 @@ void InlineList::paint(
|
|||
}
|
||||
}
|
||||
|
||||
void InlineList::validateTagBg(const QColor &color) const {
|
||||
if (!_tagBg.isNull() && _tagBgColor == color) {
|
||||
return;
|
||||
}
|
||||
_tagBgColor = color;
|
||||
float64 InlineList::TagDotAlpha() {
|
||||
return 0.6;
|
||||
}
|
||||
|
||||
QImage InlineList::PrepareTagBg(QColor tagBg, QColor dotBg) {
|
||||
const auto padding = st::reactionInlinePadding;
|
||||
const auto size = st::reactionInlineSize;
|
||||
const auto width = padding.left()
|
||||
|
@ -522,20 +521,19 @@ void InlineList::validateTagBg(const QColor &color) const {
|
|||
const auto height = padding.top() + size + padding.bottom();
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
|
||||
auto mask = QImage(
|
||||
auto result = QImage(
|
||||
QSize(width, height) * ratio,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
mask.setDevicePixelRatio(ratio);
|
||||
result.setDevicePixelRatio(ratio);
|
||||
|
||||
mask.fill(Qt::transparent);
|
||||
auto p = QPainter(&mask);
|
||||
result.fill(Qt::transparent);
|
||||
auto p = QPainter(&result);
|
||||
|
||||
auto path = QPainterPath();
|
||||
const auto arrow = st::reactionInlineTagArrow;
|
||||
const auto rradius = st::reactionInlineTagRightRadius * 1.;
|
||||
const auto radius = st::reactionInlineTagLeftRadius - rradius;
|
||||
const auto fg = QColor(255, 255, 255);
|
||||
auto pen = QPen(fg);
|
||||
auto pen = QPen(tagBg);
|
||||
pen.setWidthF(rradius * 2.);
|
||||
pen.setJoinStyle(Qt::RoundJoin);
|
||||
const auto rect = QRectF(0, 0, width, height).marginsRemoved(
|
||||
|
@ -548,9 +546,15 @@ void InlineList::validateTagBg(const QColor &color) const {
|
|||
path.lineTo(right, rect.y() + rect.height() / 2);
|
||||
path.lineTo(right - arrow, bottom);
|
||||
path.lineTo(rect.x() + radius, bottom);
|
||||
path.arcTo(QRectF(rect.x(), bottom - radius * 2, radius * 2, radius * 2), 270, -90);
|
||||
path.arcTo(
|
||||
QRectF(rect.x(), bottom - radius * 2, radius * 2, radius * 2),
|
||||
270,
|
||||
-90);
|
||||
path.lineTo(rect.x(), rect.y() + radius);
|
||||
path.arcTo(QRectF(rect.x(), rect.y(), radius * 2, radius * 2), 180, -90);
|
||||
path.arcTo(
|
||||
QRectF(rect.x(), rect.y(), radius * 2, radius * 2),
|
||||
180,
|
||||
-90);
|
||||
path.closeSubpath();
|
||||
|
||||
const auto dsize = st::reactionInlineTagDot;
|
||||
|
@ -563,16 +567,26 @@ void InlineList::validateTagBg(const QColor &color) const {
|
|||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
p.setPen(pen);
|
||||
p.setBrush(fg);
|
||||
p.setBrush(tagBg);
|
||||
p.drawPath(path);
|
||||
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(QColor(255, 255, 255, 255 * 0.6));
|
||||
p.setBrush(dotBg);
|
||||
p.drawEllipse(dot);
|
||||
|
||||
p.end();
|
||||
|
||||
_tagBg = style::colorizeImage(mask, color);
|
||||
return result;
|
||||
}
|
||||
|
||||
void InlineList::validateTagBg(const QColor &color) const {
|
||||
if (!_tagBg.isNull() && _tagBgColor == color) {
|
||||
return;
|
||||
}
|
||||
_tagBgColor = color;
|
||||
auto dot = color;
|
||||
dot.setAlphaF(dot.alphaF() * TagDotAlpha());
|
||||
_tagBg = PrepareTagBg(color, anim::with_alpha(color, TagDotAlpha()));
|
||||
}
|
||||
|
||||
void InlineList::paintSingleBg(
|
||||
|
|
|
@ -93,6 +93,9 @@ public:
|
|||
ReactionId,
|
||||
std::unique_ptr<Ui::ReactionFlyAnimation>> animations);
|
||||
|
||||
[[nodiscard]] static float64 TagDotAlpha();
|
||||
[[nodiscard]] static QImage PrepareTagBg(QColor tagBg, QColor dotBg);
|
||||
|
||||
private:
|
||||
struct Userpics {
|
||||
QImage image;
|
||||
|
|
Loading…
Add table
Reference in a new issue