ZaStoGram_desktop/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp

2163 lines
58 KiB
C++

/*
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 "chat_helpers/field_autocomplete.h"
#include "api/api_chat_participants.h"
#include "api/api_common.h"
#include "apiwrap.h"
#include "base/qt/qt_key_modifiers.h"
#include "base/random.h"
#include "base/unixtime.h"
#include "boxes/sticker_set_box.h"
#include "chat_helpers/compose/compose_show.h"
#include "chat_helpers/message_field.h" // PrepareMentionTag.
#include "chat_helpers/stickers_lottie.h"
#include "chat_helpers/tabbed_selector.h" // ChatHelpers::FileChosen.
#include "core/application.h"
#include "core/core_settings.h"
#include "data/business/data_shortcut_messages.h"
#include "data/components/recent_inline_bots.h"
#include "data/components/top_peers.h"
#include "data/stickers/data_stickers.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_file_origin.h"
#include "data/data_peer_values.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_single_player.h"
#include "main/main_session.h"
#include "media/clip/media_clip_reader.h"
#include "menu/menu_send.h" // SendMenu::FillSendMenu
#include "storage/storage_account.h"
#include "ui/effects/message_sending_animation_common.h"
#include "ui/effects/path_shift_gradient.h"
#include "ui/image/image.h"
#include "ui/text/text_options.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/tooltip.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/round_rect.h"
#include "ui/ui_utility.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_dialogs.h"
#include "styles/style_widgets.h"
#include <QtWidgets/QApplication>
namespace ChatHelpers {
namespace {
constexpr auto kEphemeralHintHoverDelay = crl::time(500);
[[nodiscard]] QString PrimaryUsername(not_null<UserData*> user) {
const auto &usernames = user->usernames();
return usernames.empty() ? user->username() : usernames.front();
}
[[nodiscard]] Data::BotStatus BotStatusFor(
ChatData *chat,
ChannelData *channel) {
return chat
? chat->botStatus
: (channel && channel->isMegagroup())
? channel->mgInfo->botStatus
: Data::BotStatus::NoBots;
}
template <typename T, typename U>
[[nodiscard]] int IndexOfInFirstN(const T &list, const U &user, int last) {
const auto count = std::min(int(list.size()), last);
for (auto i = 0; i != count; ++i) {
if (list[i].user == user) {
return i;
}
}
return -1;
}
} // namespace
class FieldAutocomplete::Inner final : public Ui::RpWidget {
public:
struct ScrollTo {
int top;
int bottom;
};
Inner(
std::shared_ptr<Show> show,
const style::EmojiPan &st,
not_null<FieldAutocomplete*> parent,
not_null<MentionRows*> mrows,
not_null<HashtagRows*> hrows,
not_null<BotCommandRows*> brows,
not_null<StickerRows*> srows);
void clearSel(bool hidden = false);
bool moveSel(int key);
bool chooseSelected(FieldAutocomplete::ChooseMethod method) const;
bool chooseAtIndex(
FieldAutocomplete::ChooseMethod method,
int index,
Api::SendOptions options = {}) const;
void setSendMenuDetails(Fn<SendMenu::Details()> &&callback);
void rowsUpdated();
[[nodiscard]] auto mentionChosen() const
-> rpl::producer<FieldAutocomplete::MentionChosen>;
[[nodiscard]] auto hashtagChosen() const
-> rpl::producer<FieldAutocomplete::HashtagChosen>;
[[nodiscard]] auto botCommandChosen() const
-> rpl::producer<FieldAutocomplete::BotCommandChosen>;
[[nodiscard]] auto stickerChosen() const
-> rpl::producer<FieldAutocomplete::StickerChosen>;
[[nodiscard]] rpl::producer<ScrollTo> scrollToRequested() const;
[[nodiscard]] rpl::producer<QRect> ephemeralIconHovered() const;
void onParentGeometryChanged();
private:
void paintEvent(QPaintEvent *e) override;
void paintStickers(Painter &p, QRect clip);
void paintRows(Painter &p, QRect clip);
void paintShadow(QPainter &p, int top);
void resizeEvent(QResizeEvent *e) override;
void enterEventHook(QEnterEvent *e) override;
void leaveEventHook(QEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void contextMenuEvent(QContextMenuEvent *e) override;
[[nodiscard]] QRect selectedRect(int index) const;
[[nodiscard]] int rowsCount() const;
[[nodiscard]] bool commandsWithUsername() const;
[[nodiscard]] bool isRemovableMentionRow(int index) const;
void updateSelectedRow();
void setSel(int sel, bool scroll = false);
void showPreview();
void selectByMouse(QPoint global);
[[nodiscard]] QRect ephemeralIconRect(int index) const;
void updateEphemeralIconHover(QPoint position);
[[nodiscard]] QSize stickerBoundingBox() const;
void setupLottie(StickerSuggestion &suggestion);
void setupWebm(StickerSuggestion &suggestion);
void repaintSticker(not_null<DocumentData*> document);
void repaintStickerAtIndex(int index);
[[nodiscard]] std::shared_ptr<Lottie::FrameRenderer> getLottieRenderer();
void clipCallback(
Media::Clip::Notification notification,
not_null<DocumentData*> document);
const std::shared_ptr<Show> _show;
const not_null<Main::Session*> _session;
const style::EmojiPan &_st;
const not_null<FieldAutocomplete*> _parent;
const not_null<MentionRows*> _mrows;
const not_null<HashtagRows*> _hrows;
const not_null<BotCommandRows*> _brows;
const not_null<StickerRows*> _srows;
Ui::RoundRect _overBg;
rpl::lifetime _stickersLifetime;
std::weak_ptr<Lottie::FrameRenderer> _lottieRenderer;
base::unique_qptr<Ui::PopupMenu> _menu;
int _stickersPerRow = 1;
int _sel = -1;
int _down = -1;
int _ephemeralIconHover = -1;
std::optional<QPoint> _lastMousePosition;
bool _mouseSelection = false;
bool _overDelete = false;
bool _previewShown = false;
bool _adjustShadowLeft = false;
const std::unique_ptr<Ui::PathShiftGradient> _pathGradient;
StickerPremiumMark _premiumMark;
Fn<SendMenu::Details()> _sendMenuDetails;
rpl::event_stream<FieldAutocomplete::MentionChosen> _mentionChosen;
rpl::event_stream<FieldAutocomplete::HashtagChosen> _hashtagChosen;
rpl::event_stream<FieldAutocomplete::BotCommandChosen> _botCommandChosen;
rpl::event_stream<FieldAutocomplete::StickerChosen> _stickerChosen;
rpl::event_stream<ScrollTo> _scrollToRequested;
rpl::event_stream<QRect> _ephemeralIconHovered;
base::Timer _previewTimer;
};
struct FieldAutocomplete::StickerSuggestion {
not_null<DocumentData*> document;
std::shared_ptr<Data::DocumentMedia> documentMedia;
std::unique_ptr<Lottie::SinglePlayer> lottie;
Media::Clip::ReaderPointer webm;
QImage premiumLock;
};
struct FieldAutocomplete::MentionRow {
enum class Source {
InlineRecent,
GuestChatTopPeer,
MentionCandidate,
};
not_null<UserData*> user;
Source source = Source::MentionCandidate;
Ui::Text::String name;
Ui::PeerUserpicView userpic;
[[nodiscard]] bool removable() const {
return (source == Source::InlineRecent)
|| (source == Source::GuestChatTopPeer);
}
};
struct FieldAutocomplete::BotCommandRow {
not_null<UserData*> user;
QString command;
QString description;
Ui::PeerUserpicView userpic;
Ui::Text::String descriptionText;
bool ephemeral = false;
};
FieldAutocomplete::FieldAutocomplete(
QWidget *parent,
std::shared_ptr<Show> show,
const style::EmojiPan *stOverride)
: RpWidget(parent)
, _show(std::move(show))
, _session(&_show->session())
, _st(stOverride ? *stOverride : st::defaultEmojiPan)
, _scroll(this)
, _ephemeralHintTimer([=] { showPendingEphemeralHint(); }) {
hide();
_scroll->setGeometry(rect());
_inner = _scroll->setOwnedWidget(
object_ptr<Inner>(
_show,
_st,
this,
&_mrows,
&_hrows,
&_brows,
&_srows));
_inner->setGeometry(rect());
_inner->scrollToRequested(
) | rpl::on_next([=](Inner::ScrollTo data) {
_scroll->scrollToY(data.top, data.bottom);
}, lifetime());
_scroll->scrollTopValue(
) | rpl::skip(1) | rpl::on_next([=] {
hideEphemeralHint();
}, lifetime());
_inner->ephemeralIconHovered(
) | rpl::on_next([=](QRect iconRect) {
ephemeralIconHovered(iconRect);
}, lifetime());
_scroll->show();
_inner->show();
hide();
_scroll->geometryChanged(
) | rpl::on_next(crl::guard(_inner, [=] {
_inner->onParentGeometryChanged();
}), lifetime());
_session->topGuestChatBots().updates(
) | rpl::on_next([=] {
if (_hiding
|| isHidden()
|| (_type != Type::Mentions)) {
return;
}
updateFiltered();
}, lifetime());
}
std::shared_ptr<Show> FieldAutocomplete::uiShow() const {
return _show;
}
void FieldAutocomplete::requestRefresh() {
_refreshRequests.fire({});
}
rpl::producer<> FieldAutocomplete::refreshRequests() const {
return _refreshRequests.events();
}
void FieldAutocomplete::requestStickersUpdate() {
_stickersUpdateRequests.fire({});
}
rpl::producer<> FieldAutocomplete::stickersUpdateRequests() const {
return _stickersUpdateRequests.events();
}
auto FieldAutocomplete::mentionChosen() const
-> rpl::producer<FieldAutocomplete::MentionChosen> {
return _inner->mentionChosen();
}
auto FieldAutocomplete::hashtagChosen() const
-> rpl::producer<FieldAutocomplete::HashtagChosen> {
return _inner->hashtagChosen();
}
auto FieldAutocomplete::botCommandChosen() const
-> rpl::producer<FieldAutocomplete::BotCommandChosen> {
return _inner->botCommandChosen();
}
auto FieldAutocomplete::stickerChosen() const
-> rpl::producer<FieldAutocomplete::StickerChosen> {
return _inner->stickerChosen();
}
auto FieldAutocomplete::choosingProcesses() const
-> rpl::producer<FieldAutocomplete::Type> {
return _scroll->scrollTopChanges(
) | rpl::filter([](int top) {
return top != 0;
}) | rpl::map([=] {
return !_mrows.empty()
? Type::Mentions
: !_hrows.empty()
? Type::Hashtags
: !_brows.empty()
? Type::BotCommands
: !_srows.empty()
? Type::Stickers
: _type;
});
}
FieldAutocomplete::~FieldAutocomplete() = default;
void FieldAutocomplete::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
const auto opacity = _opacityAnimation.value(_hiding ? 0. : 1.);
if (opacity < 1.) {
if (opacity > 0.) {
p.setOpacity(opacity);
p.drawPixmap(0, 0, _cache);
}
return;
}
p.fillRect(rect(), _st.bg);
}
void FieldAutocomplete::showFiltered(
not_null<PeerData*> peer,
QString query,
bool addInlineBots) {
_chat = peer->asChat();
_user = peer->asUser();
_channel = peer->asChannel();
if (query.isEmpty()) {
_type = Type::Mentions;
rowsUpdated(
MentionRows(),
HashtagRows(),
BotCommandRows(),
base::take(_srows),
false);
return;
}
_emoji = nullptr;
query = query.toLower();
auto type = Type::Stickers;
auto plainQuery = QStringView(query);
switch (query.at(0).unicode()) {
case '@':
type = Type::Mentions;
plainQuery = base::StringViewMid(query, 1);
break;
case '#':
type = Type::Hashtags;
plainQuery = base::StringViewMid(query, 1);
break;
case '/':
type = Type::BotCommands;
plainQuery = base::StringViewMid(query, 1);
break;
}
const auto resetScroll = (_type != type || _filter != plainQuery);
if (resetScroll) {
_type = type;
_filter = TextUtilities::RemoveAccents(plainQuery.toString());
}
_addInlineBots = addInlineBots;
updateFiltered(resetScroll);
}
void FieldAutocomplete::showStickers(EmojiPtr emoji) {
const auto resetScroll = (_emoji != emoji);
if (resetScroll || emoji) {
_emoji = emoji;
_type = Type::Stickers;
} else if (!emoji) {
rowsUpdated(
base::take(_mrows),
base::take(_hrows),
base::take(_brows),
StickerRows(),
false);
return;
}
_chat = nullptr;
_user = nullptr;
_channel = nullptr;
updateFiltered(resetScroll);
}
EmojiPtr FieldAutocomplete::stickersEmoji() const {
return _emoji;
}
bool FieldAutocomplete::clearFilteredBotCommands() {
if (_brows.empty()) {
return false;
}
_brows.clear();
return true;
}
FieldAutocomplete::StickerRows FieldAutocomplete::getStickerSuggestions() {
const auto data = &_session->data().stickers();
const auto list = data->getListByEmoji({ _emoji }, _stickersSeed);
auto result = ranges::views::all(
list
) | ranges::views::transform([](not_null<DocumentData*> sticker) {
return StickerSuggestion{
sticker,
sticker->createMediaView()
};
}) | ranges::to_vector;
for (auto &suggestion : _srows) {
if (!suggestion.lottie && !suggestion.webm) {
continue;
}
const auto i = ranges::find(
result,
suggestion.document,
&StickerSuggestion::document);
if (i != end(result)) {
i->lottie = std::move(suggestion.lottie);
i->webm = std::move(suggestion.webm);
}
}
return result;
}
void FieldAutocomplete::updateFiltered(bool resetScroll) {
const auto now = base::unixtime::now();
auto mrows = MentionRows();
auto hrows = HashtagRows();
auto brows = BotCommandRows();
auto srows = StickerRows();
if (_emoji) {
srows = getStickerSuggestions();
} else if (_type == Type::Mentions) {
using Source = MentionRow::Source;
const auto guestChatBots = _session->topGuestChatBots().list();
const auto chatMembers = !_chat
? 0
: _chat->participants.empty()
? int(_chat->lastAuthors.size())
: int(_chat->participants.size());
auto maxListSize = int(guestChatBots.size())
+ (_addInlineBots
? int(_session->recentInlineBots().list().size())
: 0);
if (_chat) {
maxListSize += chatMembers;
} else if (_channel && _channel->isMegagroup()) {
if (!_channel->canViewMembers()) {
maxListSize += _channel->mgInfo->admins.size();
} else if (!_channel->lastParticipantsRequestNeeded()) {
maxListSize += _channel->mgInfo->lastParticipants.size();
}
}
if (maxListSize) {
mrows.reserve(maxListSize);
}
const auto filterNotPassedByUsername = [&](UserData *user) {
const auto username = PrimaryUsername(user);
if (username.startsWith(_filter, Qt::CaseInsensitive)) {
const auto exactUsername
= (username.size() == _filter.size());
return exactUsername;
}
return true;
};
const auto filterNotPassedByName = [&](UserData *user) {
for (const auto &nameWord : user->nameWords()) {
if (nameWord.startsWith(_filter, Qt::CaseInsensitive)) {
const auto exactUsername = !PrimaryUsername(user).compare(
_filter,
Qt::CaseInsensitive);
return exactUsername;
}
}
return filterNotPassedByUsername(user);
};
const auto mentionUserIndex = [&](not_null<UserData*> user) {
return IndexOfInFirstN(mrows, user, int(mrows.size()));
};
const auto containsMentionUser = [&](not_null<UserData*> user) {
return mentionUserIndex(user) >= 0;
};
const auto pushMentionRow = [&](
not_null<UserData*> user,
MentionRow::Source source) {
if (containsMentionUser(user)) {
return;
}
mrows.push_back({
.user = user,
.source = source,
.userpic = user->activeUserpicView(),
});
};
const auto markMentionCandidateIfExists = [&](
not_null<UserData*> user) {
const auto index = mentionUserIndex(user);
if (index < 0) {
return false;
}
mrows[index].source = Source::MentionCandidate;
return true;
};
const auto listAllSuggestions = _filter.isEmpty();
if (_addInlineBots) {
for (const auto &user : _session->recentInlineBots().list()) {
if (user->isInaccessible()
|| (!listAllSuggestions
&& filterNotPassedByUsername(user))) {
continue;
}
pushMentionRow(user, Source::InlineRecent);
}
}
for (const auto &peer : guestChatBots) {
const auto user = peer->asUser();
if (!user
|| user->isInaccessible()
|| !user->isBot()
|| (!listAllSuggestions
&& filterNotPassedByUsername(user))
|| containsMentionUser(user)) {
continue;
}
pushMentionRow(user, Source::GuestChatTopPeer);
}
const auto skipUser = [&](not_null<UserData*> user) {
return user->isInaccessible()
|| (!listAllSuggestions && filterNotPassedByName(user));
};
if (_chat) {
auto sorted = base::flat_multi_map<TimeId, not_null<UserData*>>();
const auto byOnline = [&](not_null<UserData*> user) {
return Data::SortByOnlineValue(user, now);
};
mrows.reserve(mrows.size() + chatMembers);
if (_chat->noParticipantInfo()) {
_chat->session().api().requestFullPeer(_chat);
} else if (!_chat->participants.empty()) {
for (const auto &user : _chat->participants) {
if (skipUser(user)
|| markMentionCandidateIfExists(user)) {
continue;
}
sorted.emplace(byOnline(user), user);
}
}
for (const auto &user : _chat->lastAuthors) {
if (skipUser(user)) {
continue;
} else if (markMentionCandidateIfExists(user)) {
sorted.remove(byOnline(user), user);
continue;
}
pushMentionRow(user, Source::MentionCandidate);
sorted.remove(byOnline(user), user);
}
for (const auto &[time, user] : ranges::views::reverse(sorted)) {
pushMentionRow(user, Source::MentionCandidate);
}
} else if (_channel && _channel->isMegagroup()) {
const auto info = _channel->mgInfo.get();
const auto owner = &_channel->owner();
auto &participants = _channel->session().api().chatParticipants();
if (!_channel->canViewMembers()) {
if (!info->adminsLoaded) {
participants.requestAdmins(_channel);
} else {
mrows.reserve(mrows.size() + info->admins.size());
for (const auto &userId : info->admins) {
const auto user = owner->userLoaded(userId);
if (!user
|| skipUser(user)
|| markMentionCandidateIfExists(user)) {
continue;
}
pushMentionRow(user, Source::MentionCandidate);
}
}
} else if (_channel->lastParticipantsRequestNeeded()) {
participants.requestLast(_channel);
} else {
mrows.reserve(mrows.size() + info->lastParticipants.size());
for (const auto &user : info->lastParticipants) {
if (skipUser(user)
|| markMentionCandidateIfExists(user)) {
continue;
}
pushMentionRow(user, Source::MentionCandidate);
}
}
}
} else if (_type == Type::Hashtags) {
const auto listAllSuggestions = _filter.isEmpty();
const auto &recent = cRecentWriteHashtags();
hrows.reserve(recent.size());
for (const auto &item : recent) {
const auto &tag = item.first;
if (!listAllSuggestions
&& (tag.size() == _filter.size()
|| !TextUtilities::RemoveAccents(tag).startsWith(
_filter,
Qt::CaseInsensitive))) {
continue;
}
hrows.push_back(tag);
}
} else if (_type == Type::BotCommands) {
const auto listAllSuggestions = _filter.isEmpty();
const auto hasUsername = _filter.indexOf('@') > 0;
using Commands = std::vector<Data::BotCommand>;
auto bots = base::flat_map<not_null<UserData*>, const Commands*>();
auto count = 0;
if (_chat) {
if (_chat->noParticipantInfo()) {
_chat->session().api().requestFullPeer(_chat);
} else if (!_chat->participants.empty()) {
const auto &commands = _chat->botCommands();
for (const auto &user : _chat->participants) {
if (!user->isBot()) {
continue;
}
const auto i = commands.find(peerToUser(user->id));
if (i != end(commands)) {
bots.emplace(user, &i->second);
count += i->second.size();
}
}
}
} else if (_user && _user->isBot()) {
if (!_user->botInfo->inited) {
_user->session().api().requestFullPeer(_user);
}
count = _user->botInfo->commands.size();
bots.emplace(_user, &_user->botInfo->commands);
} else if (_channel && _channel->isMegagroup()) {
if (_channel->mgInfo->bots.empty()) {
if (_channel->mgInfo->botStatus == Data::BotStatus::Unknown) {
_channel->session().api().chatParticipants().requestBots(
_channel);
}
} else {
const auto &commands = _channel->mgInfo->botCommands();
for (const auto &user : _channel->mgInfo->bots) {
if (!user->isBot()) {
continue;
}
const auto i = commands.find(peerToUser(user->id));
if (i != end(commands)) {
bots.emplace(user, &i->second);
count += i->second.size();
}
}
}
}
if (count) {
const auto make = [&](
not_null<UserData*> user,
const Data::BotCommand &command) {
return BotCommandRow{
.user = user,
.command = command.command,
.description = command.description,
.userpic = user->activeUserpicView(),
.ephemeral = command.ephemeral,
};
};
brows.reserve(count);
const auto botStatus = BotStatusFor(_chat, _channel);
const auto withUsername = hasUsername
|| (botStatus != Data::BotStatus::NoBots);
const auto passes = [&](
not_null<UserData*> user,
const Data::BotCommand &command) {
if (listAllSuggestions) {
return true;
}
const auto toFilter = withUsername
? command.command + '@' + PrimaryUsername(user)
: command.command;
return toFilter.startsWith(_filter, Qt::CaseInsensitive);
};
if (_chat) {
for (const auto &user : _chat->lastAuthors) {
if (!user->isBot()) {
continue;
}
const auto i = bots.find(user);
if (i == end(bots)) {
continue;
}
for (const auto &command : *i->second) {
if (passes(user, command)) {
brows.push_back(make(user, command));
}
}
bots.erase(i);
}
}
for (const auto &[user, commands] : bots) {
for (const auto &command : *commands) {
if (passes(user, command)) {
brows.push_back(make(user, command));
}
}
}
}
const auto shortcuts = (_user && !_user->isBot())
? _user->owner().shortcutMessages().shortcuts().list
: base::flat_map<BusinessShortcutId, Data::Shortcut>();
if (!hasUsername && brows.empty() && !shortcuts.empty()) {
const auto self = _user->session().user();
for (const auto &[id, shortcut] : shortcuts) {
const auto &name = shortcut.name;
if (shortcut.count < 1
|| (!listAllSuggestions
&& !name.startsWith(_filter, Qt::CaseInsensitive))) {
continue;
}
brows.push_back({
.user = self,
.command = name,
.description = tr::lng_forum_messages(
tr::now,
lt_count,
shortcut.count),
.userpic = self->activeUserpicView(),
});
}
if (!brows.empty()) {
brows.insert(begin(brows), { .user = self }); // Edit.
}
}
}
rowsUpdated(
std::move(mrows),
std::move(hrows),
std::move(brows),
std::move(srows),
resetScroll);
}
void FieldAutocomplete::rowsUpdated(
MentionRows &&mrows,
HashtagRows &&hrows,
BotCommandRows &&brows,
StickerRows &&srows,
bool resetScroll) {
if (mrows.empty() && hrows.empty() && brows.empty() && srows.empty()) {
if (!isHidden()) {
hideAnimated();
}
_scroll->scrollToY(0);
_mrows.clear();
_hrows.clear();
_brows.clear();
_srows.clear();
} else {
_mrows = std::move(mrows);
_hrows = std::move(hrows);
_brows = std::move(brows);
_srows = std::move(srows);
const auto hidden = _hiding || isHidden();
if (hidden) {
show();
_scroll->show();
}
recount(resetScroll);
update();
if (hidden) {
hide();
showAnimated();
}
}
_inner->rowsUpdated();
}
void FieldAutocomplete::createEphemeralHint(QRect rect) {
const auto parent = parentWidget();
_ephemeralHint = base::make_unique_q<Ui::ImportantTooltip>(
parent,
object_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(
parent,
Ui::MakeNiceTooltipLabel(
parent,
tr::lng_ephemeral_command_tooltip(Ui::Text::WithEntities),
st::dialogsStoriesTooltipMaxWidth,
st::ttlMediaImportantTooltipLabel),
st::defaultImportantTooltip.padding),
st::dialogsStoriesTooltip);
_ephemeralHint->pointAt(rect, RectPart::Top);
_ephemeralHint->toggleAnimated(true);
}
void FieldAutocomplete::ephemeralIconHovered(QRect iconRect) {
const auto parent = parentWidget();
if (iconRect.isEmpty() || !parent || isHidden() || _hiding) {
hideEphemeralHint();
return;
}
_ephemeralHintRect = Ui::MapFrom(parent, _inner.data(), iconRect);
if (_ephemeralHint && !_ephemeralHint->isHidden()) {
_ephemeralHint->pointAt(_ephemeralHintRect, RectPart::Top);
_ephemeralHint->toggleAnimated(true);
} else {
_ephemeralHintTimer.callOnce(kEphemeralHintHoverDelay);
}
}
void FieldAutocomplete::showPendingEphemeralHint() {
if (_ephemeralHintRect.isEmpty() || isHidden() || _hiding) {
return;
}
if (_ephemeralHint) {
_ephemeralHint->pointAt(_ephemeralHintRect, RectPart::Top);
_ephemeralHint->toggleAnimated(true);
} else {
createEphemeralHint(_ephemeralHintRect);
}
}
void FieldAutocomplete::hideEphemeralHint() {
_ephemeralHintRect = QRect();
_ephemeralHintTimer.cancel();
if (_ephemeralHint) {
_ephemeralHint->toggleAnimated(false);
}
}
void FieldAutocomplete::setBoundings(QRect boundings) {
_boundings = boundings;
recount();
}
void FieldAutocomplete::recount(bool resetScroll) {
const auto oldScrollTop = _scroll->scrollTop();
const auto maxHeight = int(4.5 * st::mentionHeight);
auto height = 0;
if (!_srows.empty()) {
const auto stickersPerRow = std::max(
1,
(_boundings.width() - 2 * st::stickerPanPadding)
/ st::stickerPanSize.width());
const auto rows = rowscount(_srows.size(), stickersPerRow);
height = st::stickerPanPadding + rows * st::stickerPanSize.height();
} else if (!_mrows.empty()) {
height = _mrows.size() * st::mentionHeight;
} else if (!_hrows.empty()) {
height = _hrows.size() * st::mentionHeight;
} else if (!_brows.empty()) {
height = _brows.size() * st::mentionHeight;
}
height += _st.autocompleteBottomSkip;
if (_inner->width() != _boundings.width()
|| _inner->height() != height) {
_inner->resize(_boundings.width(), height);
}
height = std::min({ height, _boundings.height(), maxHeight });
const auto top = _boundings.y() + _boundings.height() - height;
if (width() != _boundings.width() || RpWidget::height() != height) {
setGeometry(_boundings.x(), top, _boundings.width(), height);
_scroll->resize(_boundings.width(), height);
} else if (x() != _boundings.x() || y() != top) {
move(_boundings.x(), top);
}
const auto scrollTop = resetScroll ? 0 : oldScrollTop;
if (scrollTop != oldScrollTop) {
_scroll->scrollToY(scrollTop);
}
if (resetScroll) {
_inner->clearSel();
}
}
void FieldAutocomplete::hideFast() {
hideEphemeralHint();
_ephemeralHint = nullptr;
_opacityAnimation.stop();
hideFinish();
}
void FieldAutocomplete::hideAnimated() {
if (isHidden() || _hiding) {
return;
}
hideEphemeralHint();
if (_cache.isNull()) {
_scroll->show();
_cache = Ui::GrabWidget(this);
}
_scroll->hide();
_hiding = true;
_opacityAnimation.start(
[=] { animationCallback(); },
1.,
0.,
st::emojiPanDuration);
setAttribute(Qt::WA_OpaquePaintEvent, false);
}
void FieldAutocomplete::hideFinish() {
hide();
_hiding = false;
_filter = u"-"_q;
_inner->clearSel(true);
}
void FieldAutocomplete::showAnimated() {
if (!isHidden() && !_hiding) {
return;
}
if (_cache.isNull()) {
_stickersSeed = base::RandomValue<uint64>();
_scroll->show();
_cache = Ui::GrabWidget(this);
}
_scroll->hide();
_hiding = false;
show();
_opacityAnimation.start(
[=] { animationCallback(); },
0.,
1.,
st::emojiPanDuration);
setAttribute(Qt::WA_OpaquePaintEvent, false);
}
void FieldAutocomplete::animationCallback() {
update();
if (!_opacityAnimation.animating()) {
_cache = QPixmap();
setAttribute(Qt::WA_OpaquePaintEvent);
if (_hiding) {
hideFinish();
} else {
_scroll->show();
_inner->clearSel();
}
}
}
const QString &FieldAutocomplete::filter() const {
return _filter;
}
ChatData *FieldAutocomplete::chat() const {
return _chat;
}
ChannelData *FieldAutocomplete::channel() const {
return _channel;
}
UserData *FieldAutocomplete::user() const {
return _user;
}
int FieldAutocomplete::innerTop() const {
return _scroll->scrollTop();
}
int FieldAutocomplete::innerBottom() const {
return _scroll->scrollTop() + _scroll->height();
}
bool FieldAutocomplete::chooseSelected(ChooseMethod method) const {
return _inner->chooseSelected(method);
}
void FieldAutocomplete::setSendMenuDetails(
Fn<SendMenu::Details()> &&callback) {
_inner->setSendMenuDetails(std::move(callback));
}
bool FieldAutocomplete::eventFilter(QObject *obj, QEvent *e) {
const auto hidden = isHidden();
const auto moderate = Core::App().settings().moderateModeEnabled();
if ((hidden && !moderate) || e->type() != QEvent::KeyPress) {
return QWidget::eventFilter(obj, e);
}
const auto event = static_cast<QKeyEvent*>(e);
const auto modifiers = Qt::AltModifier
| Qt::ControlModifier
| Qt::ShiftModifier
| Qt::MetaModifier;
if (event->modifiers() & modifiers) {
return QWidget::eventFilter(obj, e);
}
const auto key = event->key();
if (!hidden) {
const auto vertical = (key == Qt::Key_Up) || (key == Qt::Key_Down);
const auto horizontal = !_srows.empty()
&& ((key == Qt::Key_Left) || (key == Qt::Key_Right));
if (vertical || horizontal) {
return _inner->moveSel(key);
} else if (key == Qt::Key_Enter || key == Qt::Key_Return) {
return _inner->chooseSelected(ChooseMethod::ByEnter);
}
}
if (moderate
&& ((key >= Qt::Key_1 && key <= Qt::Key_9)
|| key == Qt::Key_Q
|| key == Qt::Key_W)) {
return _moderateKeyActivateCallback
? _moderateKeyActivateCallback(key)
: false;
}
return QWidget::eventFilter(obj, e);
}
FieldAutocomplete::Inner::Inner(
std::shared_ptr<Show> show,
const style::EmojiPan &st,
not_null<FieldAutocomplete*> parent,
not_null<MentionRows*> mrows,
not_null<HashtagRows*> hrows,
not_null<BotCommandRows*> brows,
not_null<StickerRows*> srows)
: _show(std::move(show))
, _session(&_show->session())
, _st(st)
, _parent(parent)
, _mrows(mrows)
, _hrows(hrows)
, _brows(brows)
, _srows(srows)
, _overBg(st::roundRadiusSmall, _st.overBg)
, _pathGradient(std::make_unique<Ui::PathShiftGradient>(
_st.pathBg,
_st.pathFg,
[=] { update(); }))
, _premiumMark(_session, st::stickersPremiumLock)
, _previewTimer([=] { showPreview(); }) {
_session->downloaderTaskFinished(
) | rpl::on_next([=] {
update();
}, lifetime());
_show->adjustShadowLeft(
) | rpl::on_next([=](bool adjust) {
_adjustShadowLeft = adjust;
update();
}, lifetime());
}
void FieldAutocomplete::Inner::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
const auto clip = e->rect();
if (clip != rect()) {
p.setClipRect(clip);
}
if (!_srows->empty()) {
paintStickers(p, clip);
} else {
paintRows(p, clip);
paintShadow(p, _parent->innerBottom() - st::lineWidth);
}
paintShadow(p, _parent->innerTop());
}
void FieldAutocomplete::Inner::paintShadow(QPainter &p, int top) {
const auto left = _adjustShadowLeft ? st::lineWidth : 0;
p.fillRect(left, top, width() - left, st::lineWidth, st::shadowFg);
}
void FieldAutocomplete::Inner::paintStickers(Painter &p, QRect clip) {
_pathGradient->startFrame(
0,
width(),
std::min(st::msgMaxWidth / 2, width() / 2));
const auto now = crl::now();
const auto paused = _show->paused(PauseReason::TabbedPanel);
const auto padding = st::stickerPanPadding;
const auto &single = st::stickerPanSize;
const auto rows = rowscount(_srows->size(), _stickersPerRow);
const auto [fromRow, tillRow] = Ui::RowsInRange(
clip.y() - padding,
clip.y() + clip.height() - padding,
single.height(),
rows);
const auto [fromColumn, tillColumn] = Ui::RowsInRange(
clip.x() - padding,
clip.x() + clip.width() - padding,
single.width(),
_stickersPerRow);
for (auto row = fromRow; row != tillRow; ++row) {
for (auto column = fromColumn; column != tillColumn; ++column) {
const auto index = row * _stickersPerRow + column;
if (index >= _srows->size()) {
break;
}
auto &sticker = (*_srows)[index];
const auto document = sticker.document;
const auto &media = sticker.documentMedia;
const auto info = document->sticker();
if (!info) {
continue;
}
if (media->loaded()) {
if (info->isLottie() && !sticker.lottie) {
setupLottie(sticker);
} else if (info->isWebm()
&& !sticker.webm
&& !sticker.webm.isBad()) {
setupWebm(sticker);
}
}
const auto position = QPoint(
padding + column * single.width(),
padding + row * single.height());
if (_sel == index) {
_overBg.paint(p, myrtlrect(QRect(position, single)));
}
media->checkStickerSmall();
const auto size = ComputeStickerSize(
document,
stickerBoundingBox());
const auto innerPos = position + QPoint(
(single.width() - size.width()) / 2,
(single.height() - size.height()) / 2);
auto lottieFrame = QImage();
if (sticker.lottie && sticker.lottie->ready()) {
lottieFrame = sticker.lottie->frame();
p.drawImage(
QRect(
innerPos,
lottieFrame.size() / style::DevicePixelRatio()),
lottieFrame);
if (!paused) {
sticker.lottie->markFrameShown();
}
} else if (sticker.webm && sticker.webm->started()) {
p.drawImage(
innerPos,
sticker.webm->current(
{
.frame = size,
.keepAlpha = true,
},
paused ? 0 : now));
} else if (const auto image = media->getStickerSmall()) {
p.drawPixmapLeft(innerPos, width(), image->pix(size));
} else {
PaintStickerThumbnailPath(
p,
media.get(),
QRect(innerPos, size),
_pathGradient.get());
}
if (document->isPremiumSticker()) {
_premiumMark.paint(
p,
lottieFrame,
sticker.premiumLock,
position,
single,
width());
}
}
}
}
void FieldAutocomplete::Inner::paintRows(Painter &p, QRect clip) {
const auto mentionLeft = 2 * st::mentionPadding.left()
+ st::mentionPhotoSize;
const auto mentionWidth = width()
- mentionLeft
- 2 * st::mentionPadding.right();
const auto hashtagLeft = st::historyAttach.width
+ st::historyComposeField.textMargins.left()
- st::lineWidth;
const auto hashtagWidth = width()
- st::mentionPadding.right()
- hashtagLeft
- st::defaultScrollArea.width;
const auto from = clip.top() / st::mentionHeight;
const auto till = clip.bottom() / st::mentionHeight + 1;
const auto last = rowsCount();
const auto filter = _parent->filter();
const auto withUsername = commandsWithUsername();
const auto filterSize = int(filter.size());
const auto filterIsEmpty = filter.isEmpty();
const auto &font = st::mentionFont;
for (auto i = from; i < till; ++i) {
if (i >= last) {
break;
}
const auto top = i * st::mentionHeight;
const auto textTop = top + st::mentionTop;
const auto selected = (i == _sel);
if (selected) {
p.fillRect(
0,
top,
width(),
st::mentionHeight,
st::mentionBgOver);
if (!_hrows->empty() || isRemovableMentionRow(i)) {
const auto &icon = st::smallCloseIconOver;
const auto skip = (st::mentionHeight - icon.height()) / 2;
icon.paint(
p,
QPoint(width() - icon.width() - skip, top + skip),
width());
}
}
if (!_mrows->empty()) {
auto &row = _mrows->at(i);
const auto user = row.user;
const auto username = PrimaryUsername(user);
auto first = (!filterIsEmpty
&& username.startsWith(filter, Qt::CaseInsensitive))
? ('@' + username.mid(0, filterSize))
: QString();
auto second = first.isEmpty()
? (username.isEmpty() ? QString() : ('@' + username))
: username.mid(filterSize);
const auto firstWidth = font->width(first);
const auto secondWidth = font->width(second);
auto usernameWidth = firstWidth + secondWidth;
if (row.name.isEmpty()) {
row.name.setText(
st::msgNameStyle,
user->name(),
Ui::NameTextOptions());
}
auto nameWidth = row.name.maxWidth();
if (mentionWidth < usernameWidth + nameWidth) {
nameWidth = (mentionWidth * nameWidth)
/ (nameWidth + usernameWidth);
usernameWidth = mentionWidth - nameWidth;
if (firstWidth < usernameWidth + font->elidew) {
if (firstWidth < usernameWidth) {
first = font->elided(first, usernameWidth);
} else if (!second.isEmpty()) {
first = font->elided(first + second, usernameWidth);
second = QString();
}
} else {
second = font->elided(second, usernameWidth - firstWidth);
}
}
user->loadUserpic();
user->paintUserpicLeft(
p,
row.userpic,
st::mentionPadding.left(),
top + st::mentionPadding.top(),
width(),
st::mentionPhotoSize);
p.setPen(selected ? st::mentionNameFgOver : st::mentionNameFg);
row.name.drawElided(p, mentionLeft, textTop, nameWidth);
const auto usernameLeft = mentionLeft
+ nameWidth
+ st::mentionPadding.right();
p.setFont(font);
p.setPen(selected
? st::mentionFgOverActive
: st::mentionFgActive);
p.drawText(usernameLeft, textTop + font->ascent, first);
if (!second.isEmpty()) {
p.setPen(selected ? st::mentionFgOver : st::mentionFg);
p.drawText(
usernameLeft + firstWidth,
textTop + font->ascent,
second);
}
} else if (!_hrows->empty()) {
const auto &hashtag = _hrows->at(i);
auto first = filterIsEmpty
? QString()
: ('#' + hashtag.mid(0, filterSize));
auto second = filterIsEmpty
? ('#' + hashtag)
: hashtag.mid(filterSize);
const auto firstWidth = font->width(first);
const auto secondWidth = font->width(second);
if (hashtagWidth < firstWidth + secondWidth) {
if (hashtagWidth < firstWidth + font->elidew) {
first = font->elided(first + second, hashtagWidth);
second = QString();
} else {
second = font->elided(second, hashtagWidth - firstWidth);
}
}
p.setFont(font);
if (!first.isEmpty()) {
p.setPen(selected
? st::mentionFgOverActive
: st::mentionFgActive);
p.drawText(hashtagLeft, textTop + font->ascent, first);
}
if (!second.isEmpty()) {
p.setPen(selected ? st::mentionFgOver : st::mentionFg);
p.drawText(
hashtagLeft + firstWidth,
textTop + font->ascent,
second);
}
} else {
auto &row = _brows->at(i);
const auto user = row.user;
if (user->isSelf() && row.command.isEmpty()) {
p.setPen(st::windowActiveTextFg);
p.setFont(st::semiboldFont);
p.drawText(
QRect(0, top, width(), st::mentionHeight),
tr::lng_replies_edit_button(tr::now),
style::al_center);
continue;
}
auto toHighlight = row.command;
if (withUsername) {
toHighlight += '@' + PrimaryUsername(user);
}
user->loadUserpic();
user->paintUserpicLeft(
p,
row.userpic,
st::mentionPadding.left(),
top + st::mentionPadding.top(),
width(),
st::mentionPhotoSize);
const auto commandText = '/' + toHighlight;
p.setPen(selected ? st::mentionNameFgOver : st::mentionNameFg);
p.setFont(st::semiboldFont);
p.drawText(
mentionLeft,
textTop + st::semiboldFont->ascent,
commandText);
auto addLeft = st::semiboldFont->width(commandText)
+ st::mentionPadding.left();
auto widthLeft = mentionWidth - addLeft;
if (row.ephemeral) {
const auto &icon = selected
? st::mentionEphemeralIconOver
: st::mentionEphemeralIcon;
icon.paint(
p,
mentionLeft + addLeft,
top + (st::mentionHeight - icon.height()) / 2,
width());
addLeft += icon.width() + st::mentionEphemeralIconSkip;
widthLeft -= icon.width() + st::mentionEphemeralIconSkip;
}
if (!row.description.isEmpty()
&& row.descriptionText.isEmpty()) {
row.descriptionText.setText(
st::defaultTextStyle,
row.description,
Ui::NameTextOptions());
}
if (widthLeft > font->elidew && !row.descriptionText.isEmpty()) {
p.setPen(selected ? st::mentionFgOver : st::mentionFg);
row.descriptionText.drawElided(
p,
mentionLeft + addLeft,
textTop,
widthLeft);
}
}
}
}
void FieldAutocomplete::Inner::resizeEvent(QResizeEvent *e) {
_stickersPerRow = std::max(
1,
(width() - 2 * st::stickerPanPadding) / st::stickerPanSize.width());
}
void FieldAutocomplete::Inner::mouseMoveEvent(QMouseEvent *e) {
const auto globalPosition = e->globalPos();
updateEphemeralIconHover(e->pos());
if (!_lastMousePosition) {
_lastMousePosition = globalPosition;
return;
} else if (!_mouseSelection
&& *_lastMousePosition == globalPosition) {
return;
}
selectByMouse(globalPosition);
}
void FieldAutocomplete::Inner::updateEphemeralIconHover(QPoint position) {
const auto inCommands = !_brows->empty()
&& _srows->empty()
&& _mrows->empty()
&& _hrows->empty();
const auto index = inCommands ? (position.y() / st::mentionHeight) : -1;
const auto good = (index >= 0)
&& (index < int(_brows->size()))
&& _brows->at(index).ephemeral
&& ephemeralIconRect(index).contains(position);
const auto hovered = good ? index : -1;
if (_ephemeralIconHover == hovered) {
return;
}
_ephemeralIconHover = hovered;
_ephemeralIconHovered.fire(good
? ephemeralIconRect(hovered)
: QRect());
}
QRect FieldAutocomplete::Inner::ephemeralIconRect(int index) const {
const auto &row = _brows->at(index);
if (!row.ephemeral) {
return QRect();
}
auto toHighlight = row.command;
if (commandsWithUsername()) {
toHighlight += '@' + PrimaryUsername(row.user);
}
const auto mentionLeft = 2 * st::mentionPadding.left()
+ st::mentionPhotoSize;
const auto left = mentionLeft
+ st::semiboldFont->width('/' + toHighlight)
+ st::mentionPadding.left();
const auto top = index * st::mentionHeight
+ (st::mentionHeight - st::mentionEphemeralIcon.height()) / 2;
return QRect(
left,
top,
st::mentionEphemeralIcon.width(),
st::mentionEphemeralIcon.height());
}
void FieldAutocomplete::Inner::clearSel(bool hidden) {
_overDelete = false;
_mouseSelection = false;
_lastMousePosition = std::nullopt;
setSel((_mrows->empty() && _brows->empty() && _hrows->empty())
? -1
: (_brows->size() > 1
&& _brows->front().user->isSelf()
&& _brows->front().command.isEmpty())
? 1
: 0);
if (hidden) {
_down = -1;
_previewShown = false;
}
}
bool FieldAutocomplete::Inner::moveSel(int key) {
_mouseSelection = false;
_lastMousePosition = std::nullopt;
const auto maxSel = rowsCount();
auto direction = (key == Qt::Key_Up)
? -1
: (key == Qt::Key_Down)
? 1
: 0;
if (!_srows->empty()) {
if (key == Qt::Key_Left) {
direction = -1;
} else if (key == Qt::Key_Right) {
direction = 1;
} else {
direction *= _stickersPerRow;
}
}
if (_sel >= maxSel || _sel < 0) {
if (direction < -1) {
setSel(((maxSel - 1) / _stickersPerRow) * _stickersPerRow, true);
} else if (direction < 0) {
setSel(maxSel - 1, true);
} else {
setSel(0, true);
}
return (_sel >= 0 && _sel < maxSel);
}
const auto next = _sel + direction;
setSel((next >= maxSel || next < 0) ? -1 : next, true);
return true;
}
bool FieldAutocomplete::Inner::chooseSelected(
FieldAutocomplete::ChooseMethod method) const {
return chooseAtIndex(method, _sel);
}
bool FieldAutocomplete::Inner::chooseAtIndex(
FieldAutocomplete::ChooseMethod method,
int index,
Api::SendOptions options) const {
if (index < 0 || (method == ChooseMethod::ByEnter && _mouseSelection)) {
return false;
}
if (!_srows->empty()) {
if (index < _srows->size()) {
const auto document = (*_srows)[index].document;
const auto from = [&]() -> Ui::MessageSendingAnimationFrom {
if (options.scheduled) {
return {};
}
const auto bounding = selectedRect(index);
auto contentRect = Rect(
ComputeStickerSize(document, stickerBoundingBox()));
contentRect.moveCenter(rect::center(bounding));
return {
.type = Ui::MessageSendingAnimationFrom::Type::Sticker,
.localId = _show->session().data().nextLocalMessageId(),
.globalStartGeometry = mapToGlobal(contentRect),
};
};
_stickerChosen.fire({ document, options, from() });
return true;
}
} else if (!_mrows->empty()) {
if (index < _mrows->size()) {
const auto user = _mrows->at(index).user;
_mentionChosen.fire({ user, PrimaryUsername(user), method });
return true;
}
} else if (!_hrows->empty()) {
if (index < _hrows->size()) {
_hashtagChosen.fire({ '#' + _hrows->at(index), method });
return true;
}
} else if (!_brows->empty()) {
if (index < _brows->size()) {
const auto user = _brows->at(index).user;
const auto &command = _brows->at(index).command;
const auto commandString = u"/%1%2"_q.arg(
command,
commandsWithUsername()
? ('@' + PrimaryUsername(user))
: QString());
_botCommandChosen.fire({ user, commandString, method });
return true;
}
}
return false;
}
bool FieldAutocomplete::Inner::isRemovableMentionRow(int index) const {
return (index >= 0)
&& (index < _mrows->size())
&& _mrows->at(index).removable();
}
void FieldAutocomplete::Inner::mousePressEvent(QMouseEvent *e) {
selectByMouse(e->globalPos());
if (e->button() == Qt::LeftButton) {
if (_overDelete
&& (_mrows->empty()
? (_sel >= 0 && _sel < _hrows->size())
: isRemovableMentionRow(_sel))) {
auto writeRecent = false;
if (_mrows->empty()) {
const auto toRemove = _hrows->at(_sel);
auto &recent = cRefRecentWriteHashtags();
for (auto i = recent.begin(); i != recent.cend();) {
if (i->first == toRemove) {
i = recent.erase(i);
writeRecent = true;
} else {
++i;
}
}
} else {
const auto &row = _mrows->at(_sel);
switch (row.source) {
case MentionRow::Source::InlineRecent:
_session->recentInlineBots().remove(row.user);
break;
case MentionRow::Source::GuestChatTopPeer:
_session->topGuestChatBots().remove(row.user);
break;
case MentionRow::Source::MentionCandidate:
break;
}
}
if (writeRecent) {
_show->session().local().writeRecentHashtagsAndBots();
}
_parent->updateFiltered();
selectByMouse(e->globalPos());
} else if (_srows->empty()) {
chooseSelected(FieldAutocomplete::ChooseMethod::ByClick);
} else {
_down = _sel;
_previewTimer.callOnce(QApplication::startDragTime());
}
}
}
void FieldAutocomplete::Inner::mouseReleaseEvent(QMouseEvent *e) {
_previewTimer.cancel();
const auto pressed = std::exchange(_down, -1);
selectByMouse(e->globalPos());
if (_previewShown) {
_previewShown = false;
return;
} else if (_sel < 0 || _sel != pressed || _srows->empty()) {
return;
}
chooseSelected(FieldAutocomplete::ChooseMethod::ByClick);
}
void FieldAutocomplete::Inner::contextMenuEvent(QContextMenuEvent *e) {
if (_sel < 0 || _srows->empty() || _down >= 0) {
return;
}
const auto index = _sel;
const auto details = _sendMenuDetails
? _sendMenuDetails()
: SendMenu::Details();
const auto method = FieldAutocomplete::ChooseMethod::ByClick;
_menu = base::make_unique_q<Ui::PopupMenu>(
this,
st::popupMenuWithIcons);
const auto send = crl::guard(this, [=](Api::SendOptions options) {
chooseAtIndex(method, index, options);
});
SendMenu::FillSendMenu(
_menu,
_show,
details,
SendMenu::DefaultCallback(_show, send));
if (!_menu->empty()) {
_menu->popup(QCursor::pos());
}
}
void FieldAutocomplete::Inner::enterEventHook(QEnterEvent *e) {
setMouseTracking(true);
}
void FieldAutocomplete::Inner::leaveEventHook(QEvent *e) {
setMouseTracking(false);
if (_ephemeralIconHover >= 0) {
_ephemeralIconHover = -1;
_ephemeralIconHovered.fire(QRect());
}
if (_mouseSelection) {
setSel(-1);
_mouseSelection = false;
_lastMousePosition = std::nullopt;
}
}
QRect FieldAutocomplete::Inner::selectedRect(int index) const {
if (index < 0) {
return QRect();
}
if (_srows->empty()) {
return { 0, index * st::mentionHeight, width(), st::mentionHeight };
}
const auto row = index / _stickersPerRow;
const auto column = index % _stickersPerRow;
return QRect(
st::stickerPanPadding + column * st::stickerPanSize.width(),
st::stickerPanPadding + row * st::stickerPanSize.height(),
st::stickerPanSize.width(),
st::stickerPanSize.height());
}
int FieldAutocomplete::Inner::rowsCount() const {
return !_mrows->empty()
? int(_mrows->size())
: !_hrows->empty()
? int(_hrows->size())
: !_brows->empty()
? int(_brows->size())
: int(_srows->size());
}
bool FieldAutocomplete::Inner::commandsWithUsername() const {
const auto botStatus = BotStatusFor(_parent->chat(), _parent->channel());
return (botStatus != Data::BotStatus::NoBots)
|| (_parent->filter().indexOf('@') > 0);
}
void FieldAutocomplete::Inner::updateSelectedRow() {
const auto rect = selectedRect(_sel);
if (rect.isValid()) {
update(rect);
}
}
void FieldAutocomplete::Inner::setSel(int sel, bool scroll) {
updateSelectedRow();
_sel = sel;
updateSelectedRow();
if (scroll && _sel >= 0) {
if (_srows->empty()) {
_scrollToRequested.fire({
_sel * st::mentionHeight,
(_sel + 1) * st::mentionHeight });
} else {
const auto row = _sel / _stickersPerRow;
const auto padding = st::stickerPanPadding;
_scrollToRequested.fire({
(row ? padding : 0) + row * st::stickerPanSize.height(),
(padding
+ (row + 1) * st::stickerPanSize.height()
+ _st.autocompleteBottomSkip) });
}
}
}
void FieldAutocomplete::Inner::rowsUpdated() {
if (_srows->empty()) {
_stickersLifetime.destroy();
}
if (_ephemeralIconHover >= 0) {
_ephemeralIconHover = -1;
_ephemeralIconHovered.fire(QRect());
}
}
auto FieldAutocomplete::Inner::getLottieRenderer()
-> std::shared_ptr<Lottie::FrameRenderer> {
if (auto result = _lottieRenderer.lock()) {
return result;
}
auto result = Lottie::MakeFrameRenderer();
_lottieRenderer = result;
return result;
}
void FieldAutocomplete::Inner::setupLottie(StickerSuggestion &suggestion) {
const auto document = suggestion.document;
suggestion.lottie = LottiePlayerFromDocument(
suggestion.documentMedia.get(),
StickerLottieSize::InlineResults,
stickerBoundingBox() * style::DevicePixelRatio(),
Lottie::Quality::Default,
getLottieRenderer());
suggestion.lottie->updates(
) | rpl::on_next([=] {
repaintSticker(document);
}, _stickersLifetime);
}
void FieldAutocomplete::Inner::setupWebm(StickerSuggestion &suggestion) {
const auto document = suggestion.document;
auto callback = [=](Media::Clip::Notification notification) {
clipCallback(notification, document);
};
suggestion.webm = Media::Clip::MakeReader(
suggestion.documentMedia->owner()->location(),
suggestion.documentMedia->bytes(),
std::move(callback));
}
QSize FieldAutocomplete::Inner::stickerBoundingBox() const {
return QSize(
st::stickerPanSize.width() - st::roundRadiusSmall * 2,
st::stickerPanSize.height() - st::roundRadiusSmall * 2);
}
void FieldAutocomplete::Inner::repaintSticker(
not_null<DocumentData*> document) {
const auto i = ranges::find(
*_srows,
document,
&StickerSuggestion::document);
if (i == end(*_srows)) {
return;
}
repaintStickerAtIndex(i - begin(*_srows));
}
void FieldAutocomplete::Inner::repaintStickerAtIndex(int index) {
update(selectedRect(index));
}
void FieldAutocomplete::Inner::clipCallback(
Media::Clip::Notification notification,
not_null<DocumentData*> document) {
const auto i = ranges::find(
*_srows,
document,
&StickerSuggestion::document);
if (i == end(*_srows)) {
return;
}
using namespace Media::Clip;
switch (notification) {
case Notification::Reinit: {
if (!i->webm) {
break;
} else if (i->webm->state() == State::Error) {
i->webm.setBad();
} else if (i->webm->ready() && !i->webm->started()) {
const auto size = ComputeStickerSize(
i->document,
stickerBoundingBox());
i->webm->start({ .frame = size, .keepAlpha = true });
}
} break;
case Notification::Repaint: break;
}
repaintStickerAtIndex(i - begin(*_srows));
}
void FieldAutocomplete::Inner::selectByMouse(QPoint globalPosition) {
_mouseSelection = true;
_lastMousePosition = globalPosition;
const auto mouse = mapFromGlobal(globalPosition);
if (_down >= 0 && !_previewShown) {
return;
}
auto sel = -1;
auto maxSel = 0;
if (!_srows->empty()) {
const auto padding = st::stickerPanPadding;
const auto row = (mouse.y() >= padding)
? (mouse.y() - padding) / st::stickerPanSize.height()
: -1;
const auto column = (mouse.x() >= padding)
? (mouse.x() - padding) / st::stickerPanSize.width()
: -1;
if (row >= 0 && column >= 0) {
sel = row * _stickersPerRow + column;
}
maxSel = _srows->size();
_overDelete = false;
} else {
sel = mouse.y() / st::mentionHeight;
maxSel = rowsCount();
_overDelete = (!_hrows->empty() || isRemovableMentionRow(sel))
? (mouse.x() >= width() - st::mentionHeight)
: false;
}
if (sel < 0 || sel >= maxSel) {
sel = -1;
}
if (sel != _sel) {
setSel(sel);
if (_down >= 0 && _sel >= 0 && _down != _sel) {
_down = _sel;
showPreview();
}
}
}
void FieldAutocomplete::Inner::onParentGeometryChanged() {
const auto globalPosition = QCursor::pos();
if (rect().contains(mapFromGlobal(globalPosition))) {
setMouseTracking(true);
if (_mouseSelection) {
selectByMouse(globalPosition);
}
}
}
void FieldAutocomplete::Inner::showPreview() {
if (_down >= 0 && _down < _srows->size()) {
const auto document = (*_srows)[_down].document;
_show->showMediaPreview(document->stickerSetOrigin(), document);
_previewShown = true;
}
}
void FieldAutocomplete::Inner::setSendMenuDetails(
Fn<SendMenu::Details()> &&callback) {
_sendMenuDetails = std::move(callback);
}
auto FieldAutocomplete::Inner::mentionChosen() const
-> rpl::producer<FieldAutocomplete::MentionChosen> {
return _mentionChosen.events();
}
auto FieldAutocomplete::Inner::hashtagChosen() const
-> rpl::producer<FieldAutocomplete::HashtagChosen> {
return _hashtagChosen.events();
}
auto FieldAutocomplete::Inner::botCommandChosen() const
-> rpl::producer<FieldAutocomplete::BotCommandChosen> {
return _botCommandChosen.events();
}
auto FieldAutocomplete::Inner::stickerChosen() const
-> rpl::producer<FieldAutocomplete::StickerChosen> {
return _stickerChosen.events();
}
auto FieldAutocomplete::Inner::scrollToRequested() const
-> rpl::producer<ScrollTo> {
return _scrollToRequested.events();
}
rpl::producer<QRect> FieldAutocomplete::Inner::ephemeralIconHovered() const {
return _ephemeralIconHovered.events();
}
void InitFieldAutocomplete(
std::unique_ptr<FieldAutocomplete> &autocomplete,
FieldAutocompleteDescriptor &&descriptor) {
Expects(!autocomplete);
autocomplete = std::make_unique<FieldAutocomplete>(
descriptor.parent,
descriptor.show,
descriptor.stOverride);
const auto raw = autocomplete.get();
const auto field = descriptor.field;
field->rawTextEdit()->installEventFilter(raw);
raw->mentionChosen(
) | rpl::on_next([=](FieldAutocomplete::MentionChosen data) {
const auto user = data.user;
const auto ctrlClick = base::IsCtrlPressed()
&& data.method == FieldAutocomplete::ChooseMethod::ByClick;
if (data.mention.isEmpty() || ctrlClick) {
field->insertTag(
user->firstName.isEmpty() ? user->name() : user->firstName,
PrepareMentionTag(user));
} else {
field->insertTag('@' + data.mention);
}
}, raw->lifetime());
const auto sendCommand = descriptor.sendBotCommand;
const auto setText = descriptor.setText;
raw->hashtagChosen(
) | rpl::on_next([=](FieldAutocomplete::HashtagChosen data) {
field->insertTag(data.hashtag);
}, raw->lifetime());
const auto peer = descriptor.peer;
const auto features = descriptor.features;
const auto processShortcut = descriptor.processShortcut;
const auto shortcutMessages = (processShortcut != nullptr)
? &peer->owner().shortcutMessages()
: nullptr;
raw->botCommandChosen(
) | rpl::on_next([=](FieldAutocomplete::BotCommandChosen data) {
if (!features().autocompleteCommands) {
return;
}
using Method = FieldAutocompleteChooseMethod;
const auto byTab = (data.method == Method::ByTab);
const auto shortcut = data.user->isSelf();
// Send bot command at once, if it was not inserted by pressing Tab.
if (byTab && data.command.size() > 1) {
field->insertTag(data.command);
} else if (!shortcut) {
sendCommand(data.command);
setText(
field->getTextWithTagsPart(field->textCursor().position()));
} else if (processShortcut) {
processShortcut(data.command.mid(1));
}
}, raw->lifetime());
raw->setModerateKeyActivateCallback(
std::move(descriptor.moderateKeyActivateCallback));
if (const auto stickerChoosing = descriptor.stickerChoosing) {
raw->choosingProcesses(
) | rpl::on_next([=](FieldAutocomplete::Type type) {
if (type == FieldAutocomplete::Type::Stickers) {
stickerChoosing();
}
}, raw->lifetime());
}
if (const auto chosen = descriptor.stickerChosen) {
raw->stickerChosen(
) | rpl::on_next(chosen, raw->lifetime());
}
field->tabbed(
) | rpl::on_next([=](not_null<Ui::InputField::TabbedRequest*> request) {
if (!raw->isHidden()) {
raw->chooseSelected(FieldAutocomplete::ChooseMethod::ByTab);
request->handled = true;
}
}, raw->lifetime());
const auto check = [=] {
auto parsed = ParseMentionHashtagBotCommandQuery(field, features());
if (parsed.query.isEmpty()) {
} else if (parsed.query[0] == '#'
&& cRecentWriteHashtags().isEmpty()
&& cRecentSearchHashtags().isEmpty()) {
peer->session().local().readRecentHashtagsAndBots();
} else if (parsed.query[0] == '@'
&& peer->session().recentInlineBots().list().empty()) {
peer->session().local().readRecentHashtagsAndBots();
} else if (parsed.query[0] == '/'
&& peer->isUser()
&& !peer->asUser()->isBot()
&& (!shortcutMessages
|| shortcutMessages->shortcuts().list.empty()
|| peer->starsPerMessageChecked() != 0)) {
parsed = {};
}
if (!parsed.query.isEmpty() && parsed.query[0] == '@') {
peer->session().topGuestChatBots().reload();
}
raw->showFiltered(peer, parsed.query, parsed.fromStart);
};
const auto updateStickersByEmoji = [=] {
const auto errorForStickers = Data::RestrictionError(
peer,
ChatRestriction::SendStickers);
if (features().suggestStickersByEmoji && !errorForStickers) {
const auto &text = field->getTextWithTags().text;
auto length = 0;
if (const auto emoji = Ui::Emoji::Find(text, &length)) {
if (text.size() <= length) {
raw->showStickers(emoji);
return;
}
}
}
raw->showStickers(nullptr);
};
raw->refreshRequests(
) | rpl::on_next(check, raw->lifetime());
raw->stickersUpdateRequests(
) | rpl::on_next(updateStickersByEmoji, raw->lifetime());
peer->owner().botCommandsChanges(
) | rpl::filter([=](not_null<PeerData*> changed) {
return (peer == changed);
}) | rpl::on_next([=] {
if (raw->clearFilteredBotCommands()) {
check();
}
}, raw->lifetime());
peer->owner().stickers().updated(
Data::StickersType::Stickers
) | rpl::on_next(updateStickersByEmoji, raw->lifetime());
QObject::connect(
field->rawTextEdit(),
&QTextEdit::cursorPositionChanged,
raw,
check,
Qt::QueuedConnection);
field->changes(
) | rpl::on_next(updateStickersByEmoji, raw->lifetime());
peer->session().changes().peerUpdates(
Data::PeerUpdate::Flag::Rights
) | rpl::filter([=](const Data::PeerUpdate &update) {
return (update.peer == peer);
}) | rpl::on_next(updateStickersByEmoji, raw->lifetime());
if (shortcutMessages) {
shortcutMessages->shortcutsChanged(
) | rpl::on_next(check, raw->lifetime());
}
raw->setSendMenuDetails(std::move(descriptor.sendMenuDetails));
raw->hideFast();
}
} // namespace ChatHelpers