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,49 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "chat_helpers/bot_command.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_peer.h"
#include "data/data_user.h"
#include "data/data_session.h"
#include "history/history_item.h"
namespace Bot {
QString WrapCommandInChat(
not_null<PeerData*> peer,
const QString &command,
const FullMsgId &context) {
auto result = command;
if (const auto item = peer->owner().message(context)) {
if (const auto user = item->fromOriginal()->asUser()) {
return WrapCommandInChat(peer, command, user);
}
}
return result;
}
QString WrapCommandInChat(
not_null<PeerData*> peer,
const QString &command,
not_null<UserData*> bot) {
if (!bot->isBot() || bot->username().isEmpty()) {
return command;
}
const auto botStatus = peer->isChat()
? peer->asChat()->botStatus
: peer->isMegagroup()
? peer->asChannel()->mgInfo->botStatus
: -1;
return ((command.indexOf('@') < 2) && (botStatus == 0 || botStatus == 2))
? command + '@' + bot->username()
: command;
}
} // namespace Bot

View File

@@ -0,0 +1,31 @@
/*
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 PeerData;
class UserData;
namespace Bot {
struct SendCommandRequest {
not_null<PeerData*> peer;
QString command;
FullMsgId context;
FullReplyTo replyTo;
};
[[nodiscard]] QString WrapCommandInChat(
not_null<PeerData*> peer,
const QString &command,
const FullMsgId &context);
[[nodiscard]] QString WrapCommandInChat(
not_null<PeerData*> peer,
const QString &command,
not_null<UserData*> bot);
} // namespace Bot

View File

@@ -0,0 +1,392 @@
/*
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/bot_keyboard.h"
#include "api/api_bot.h"
#include "core/click_handler_types.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "history/history.h"
#include "history/history_item_components.h"
#include "main/main_session.h"
#include "ui/cached_round_corners.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_widgets.h"
namespace {
class Style : public ReplyKeyboard::Style {
public:
Style(
not_null<BotKeyboard*> parent,
const style::BotKeyboardButton &st);
Images::CornersMaskRef buttonRounding(
Ui::BubbleRounding outer,
RectParts sides) const override;
void startPaint(QPainter &p, const Ui::ChatStyle *st) const override;
const style::TextStyle &textStyle() const override;
void repaint(not_null<const HistoryItem*> item) const override;
protected:
void paintButtonBg(
QPainter &p,
const Ui::ChatStyle *st,
const QRect &rect,
Ui::BubbleRounding rounding,
float64 howMuchOver) const override;
void paintButtonIcon(
QPainter &p,
const Ui::ChatStyle *st,
const QRect &rect,
int outerWidth,
HistoryMessageMarkupButton::Type type) const override;
void paintButtonLoading(
QPainter &p,
const Ui::ChatStyle *st,
const QRect &rect,
int outerWidth,
Ui::BubbleRounding rounding) const override;
int minButtonWidth(HistoryMessageMarkupButton::Type type) const override;
private:
not_null<BotKeyboard*> _parent;
};
Style::Style(
not_null<BotKeyboard*> parent,
const style::BotKeyboardButton &st)
: ReplyKeyboard::Style(st), _parent(parent) {
}
void Style::startPaint(QPainter &p, const Ui::ChatStyle *st) const {
p.setPen(st::botKbColor);
p.setFont(st::botKbStyle.font);
}
const style::TextStyle &Style::textStyle() const {
return st::botKbStyle;
}
void Style::repaint(not_null<const HistoryItem*> item) const {
_parent->update();
}
Images::CornersMaskRef Style::buttonRounding(
Ui::BubbleRounding outer,
RectParts sides) const {
using namespace Images;
return CornersMaskRef(CornersMask(ImageRoundRadius::Small));
}
void Style::paintButtonBg(
QPainter &p,
const Ui::ChatStyle *st,
const QRect &rect,
Ui::BubbleRounding rounding,
float64 howMuchOver) const {
Ui::FillRoundRect(p, rect, st::botKbBg, Ui::BotKeyboardCorners);
}
void Style::paintButtonIcon(
QPainter &p,
const Ui::ChatStyle *st,
const QRect &rect,
int outerWidth,
HistoryMessageMarkupButton::Type type) const {
// Buttons with icons should not appear here.
}
void Style::paintButtonLoading(
QPainter &p,
const Ui::ChatStyle *st,
const QRect &rect,
int outerWidth,
Ui::BubbleRounding rounding) const {
// Buttons with loading progress should not appear here.
}
int Style::minButtonWidth(HistoryMessageMarkupButton::Type type) const {
int result = 2 * buttonPadding();
return result;
}
} // namespace
BotKeyboard::BotKeyboard(
not_null<Window::SessionController*> controller,
QWidget *parent)
: RpWidget(parent)
, _controller(controller)
, _st(&st::botKbButton) {
setGeometry(0, 0, _st->margin, st::botKbScroll.deltat);
_height = st::botKbScroll.deltat;
setMouseTracking(true);
}
void BotKeyboard::paintEvent(QPaintEvent *e) {
Painter p(this);
auto clip = e->rect();
p.fillRect(clip, st::historyComposeAreaBg);
if (_impl) {
int x = rtl() ? st::botKbScroll.width : _st->margin;
p.translate(x, st::botKbScroll.deltat);
_impl->paint(
p,
nullptr,
Ui::BubbleRounding(),
width(),
clip.translated(-x, -st::botKbScroll.deltat));
}
}
void BotKeyboard::mousePressEvent(QMouseEvent *e) {
_lastMousePos = e->globalPos();
updateSelected();
ClickHandler::pressed();
}
void BotKeyboard::mouseMoveEvent(QMouseEvent *e) {
_lastMousePos = e->globalPos();
updateSelected();
}
void BotKeyboard::mouseReleaseEvent(QMouseEvent *e) {
_lastMousePos = e->globalPos();
updateSelected();
if (ClickHandlerPtr activated = ClickHandler::unpressed()) {
ActivateClickHandler(window(), activated, {
e->button(),
QVariant::fromValue(ClickHandlerContext{
.sessionWindow = base::make_weak(_controller),
})
});
}
}
void BotKeyboard::enterEventHook(QEnterEvent *e) {
_lastMousePos = QCursor::pos();
updateSelected();
}
void BotKeyboard::leaveEventHook(QEvent *e) {
clearSelection();
}
bool BotKeyboard::moderateKeyActivate(
int key,
Fn<ClickContext(FullMsgId)> context) {
const auto &data = _controller->session().data();
const auto botCommand = [](int key) {
if (key == Qt::Key_Q || key == Qt::Key_6) {
return u"/translate"_q;
} else if (key == Qt::Key_W || key == Qt::Key_5) {
return u"/eng"_q;
} else if (key == Qt::Key_3) {
return u"/pattern"_q;
} else if (key == Qt::Key_4) {
return u"/abuse"_q;
} else if (key == Qt::Key_0 || key == Qt::Key_E || key == Qt::Key_9) {
return u"/undo"_q;
} else if (key == Qt::Key_Plus
|| key == Qt::Key_QuoteLeft
|| key == Qt::Key_7) {
return u"/next"_q;
} else if (key == Qt::Key_Period
|| key == Qt::Key_S
|| key == Qt::Key_8) {
return u"/stats"_q;
}
return QString();
};
if (const auto item = data.message(_wasForMsgId)) {
if (const auto markup = item->Get<HistoryMessageReplyMarkup>()) {
if (key >= Qt::Key_1 && key <= Qt::Key_2) {
const auto index = int(key - Qt::Key_1);
if (!markup->data.rows.empty()
&& index >= 0
&& index < int(markup->data.rows.front().size())) {
Api::ActivateBotCommand(
context(
_wasForMsgId).other.value<ClickHandlerContext>(),
0,
index);
return true;
}
} else if (const auto user = item->history()->peer->asUser()) {
if (user->isBot() && item->from() == user) {
const auto command = botCommand(key);
if (!command.isEmpty()) {
_sendCommandRequests.fire({
.peer = user,
.command = command,
.context = item->fullId(),
});
}
return true;
}
}
}
}
return false;
}
void BotKeyboard::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) {
if (!_impl) return;
_impl->clickHandlerActiveChanged(p, active);
}
void BotKeyboard::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {
if (!_impl) return;
_impl->clickHandlerPressedChanged(p, pressed, Ui::BubbleRounding());
}
bool BotKeyboard::updateMarkup(HistoryItem *to, bool force) {
if (!to || !to->definesReplyKeyboard()) {
if (_wasForMsgId.msg) {
_maximizeSize = _singleUse = _forceReply = _persistent = false;
_wasForMsgId = FullMsgId();
_placeholder = QString();
_impl = nullptr;
return true;
}
return false;
}
const auto peerId = to->history()->peer->id;
if (_wasForMsgId == FullMsgId(peerId, to->id) && !force) {
return false;
}
_wasForMsgId = FullMsgId(peerId, to->id);
auto markupFlags = to->replyKeyboardFlags();
_forceReply = markupFlags & ReplyMarkupFlag::ForceReply;
_maximizeSize = !(markupFlags & ReplyMarkupFlag::Resize);
_singleUse = _forceReply || (markupFlags & ReplyMarkupFlag::SingleUse);
_persistent = (markupFlags & ReplyMarkupFlag::Persistent);
if (const auto markup = to->Get<HistoryMessageReplyMarkup>()) {
_placeholder = markup->data.placeholder;
} else {
_placeholder = QString();
}
_impl = nullptr;
if (auto markup = to->Get<HistoryMessageReplyMarkup>()) {
if (!markup->data.rows.empty()) {
_impl = std::make_unique<ReplyKeyboard>(
to,
std::make_unique<Style>(this, *_st));
}
}
resizeToWidth(width(), _maxOuterHeight);
return true;
}
bool BotKeyboard::hasMarkup() const {
return _impl != nullptr;
}
bool BotKeyboard::forceReply() const {
return _forceReply;
}
int BotKeyboard::resizeGetHeight(int newWidth) {
updateStyle(newWidth);
_height = st::botKbScroll.deltat + st::botKbScroll.deltab + (_impl ? _impl->naturalHeight() : 0);
if (_maximizeSize) {
accumulate_max(_height, _maxOuterHeight);
}
if (_impl) {
int implWidth = newWidth - _st->margin - st::botKbScroll.width;
int implHeight = _height - (st::botKbScroll.deltat + st::botKbScroll.deltab);
_impl->resize(implWidth, implHeight);
}
return _height;
}
bool BotKeyboard::maximizeSize() const {
return _maximizeSize;
}
bool BotKeyboard::singleUse() const {
return _singleUse;
}
bool BotKeyboard::persistent() const {
return _persistent;
}
void BotKeyboard::updateStyle(int newWidth) {
if (!_impl) return;
int implWidth = newWidth - st::botKbButton.margin - st::botKbScroll.width;
_st = _impl->isEnoughSpace(implWidth, st::botKbButton) ? &st::botKbButton : &st::botKbTinyButton;
_impl->setStyle(std::make_unique<Style>(this, *_st));
}
void BotKeyboard::clearSelection() {
if (_impl) {
if (ClickHandler::setActive(ClickHandlerPtr(), this)) {
Ui::Tooltip::Hide();
setCursor(style::cur_default);
}
}
}
QPoint BotKeyboard::tooltipPos() const {
return _lastMousePos;
}
bool BotKeyboard::tooltipWindowActive() const {
return Ui::AppInFocus() && Ui::InFocusChain(window());
}
QString BotKeyboard::tooltipText() const {
if (ClickHandlerPtr lnk = ClickHandler::getActive()) {
return lnk->tooltip();
}
return QString();
}
void BotKeyboard::updateSelected() {
Ui::Tooltip::Show(1000, this);
if (!_impl) return;
auto p = mapFromGlobal(_lastMousePos);
auto x = rtl() ? st::botKbScroll.width : _st->margin;
auto link = _impl->getLink(p - QPoint(x, _st->margin));
if (ClickHandler::setActive(link, this)) {
Ui::Tooltip::Hide();
setCursor(link ? style::cur_pointer : style::cur_default);
}
}
auto BotKeyboard::sendCommandRequests() const
-> rpl::producer<Bot::SendCommandRequest> {
return _sendCommandRequests.events();
}
BotKeyboard::~BotKeyboard() = default;

View File

@@ -0,0 +1,104 @@
/*
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/tooltip.h"
#include "chat_helpers/bot_command.h"
class ReplyKeyboard;
namespace style {
struct BotKeyboardButton;
} // namespace style
namespace Window {
class SessionController;
} // namespace Window
class BotKeyboard
: public Ui::RpWidget
, public Ui::AbstractTooltipShower
, public ClickHandlerHost {
public:
BotKeyboard(
not_null<Window::SessionController*> controller,
QWidget *parent);
bool moderateKeyActivate(int index, Fn<ClickContext(FullMsgId)> context);
// With force=true the markup is updated even if it is
// already shown for the passed history item.
bool updateMarkup(HistoryItem *last, bool force = false);
[[nodiscard]] bool hasMarkup() const;
[[nodiscard]] bool forceReply() const;
[[nodiscard]] QString placeholder() const {
return _placeholder;
}
void step_selected(crl::time ms, bool timer);
void resizeToWidth(int newWidth, int maxOuterHeight) {
_maxOuterHeight = maxOuterHeight;
return RpWidget::resizeToWidth(newWidth);
}
[[nodiscard]] bool maximizeSize() const;
[[nodiscard]] bool singleUse() const;
[[nodiscard]] bool persistent() const;
[[nodiscard]] FullMsgId forMsgId() const {
return _wasForMsgId;
}
// AbstractTooltipShower interface
QString tooltipText() const override;
QPoint tooltipPos() const override;
bool tooltipWindowActive() const override;
// ClickHandlerHost interface
void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override;
void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override;
rpl::producer<Bot::SendCommandRequest> sendCommandRequests() const;
~BotKeyboard();
protected:
int resizeGetHeight(int newWidth) override;
void paintEvent(QPaintEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void enterEventHook(QEnterEvent *e) override;
void leaveEventHook(QEvent *e) override;
private:
void updateSelected();
void updateStyle(int newWidth);
void clearSelection();
const not_null<Window::SessionController*> _controller;
FullMsgId _wasForMsgId;
QString _placeholder;
int _height = 0;
int _maxOuterHeight = 0;
bool _maximizeSize = false;
bool _singleUse = false;
bool _forceReply = false;
bool _persistent = false;
QPoint _lastMousePos;
std::unique_ptr<ReplyKeyboard> _impl;
rpl::event_stream<Bot::SendCommandRequest> _sendCommandRequests;
const style::BotKeyboardButton *_st = nullptr;
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
/*
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 ChatHelpers {
struct ComposeFeatures {
bool likes : 1 = false;
bool sendAs : 1 = true;
bool ttlInfo : 1 = true;
bool attachments : 1 = true;
bool botCommandSend : 1 = true;
bool silentBroadcastToggle : 1 = true;
bool attachBotsMenu : 1 = true;
bool inlineBots : 1 = true;
bool megagroupSet : 1 = true;
bool collectibleStatus : 1 = false;
bool stickersSettings : 1 = true;
bool openStickerSets : 1 = true;
bool autocompleteHashtags : 1 = true;
bool autocompleteMentions : 1 = true;
bool autocompleteCommands : 1 = true;
bool suggestStickersByEmoji : 1 = true;
bool commonTabbedPanel : 1 = true;
bool recordMediaMessage : 1 = true;
bool editMessageStars : 1 = false;
bool emojiOnlyPanel : 1 = false;
bool videoStream : 1 = false;
};
} // namespace ChatHelpers

View File

@@ -0,0 +1,52 @@
/*
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/compose/compose_show.h"
#include "core/application.h"
#include "main/main_session.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
namespace ChatHelpers {
rpl::producer<bool> Show::adjustShadowLeft() const {
return rpl::single(false);
}
ResolveWindow ResolveWindowDefault() {
return [](not_null<Main::Session*> session)
-> Window::SessionController* {
const auto check = [&](Window::Controller *window) {
if (const auto controller = window->sessionController()) {
if (&controller->session() == session) {
return controller;
}
}
return (Window::SessionController*)nullptr;
};
auto &app = Core::App();
const auto account = not_null(&session->account());
if (const auto a = check(app.activeWindow())) {
return a;
} else if (const auto b = check(app.activePrimaryWindow())) {
return b;
} else if (const auto c = check(app.windowFor(account))) {
return c;
} else if (const auto d = check(app.ensureSeparateWindowFor(
account))) {
return d;
}
return nullptr;
};
}
Window::SessionController *Show::resolveWindow() const {
return ResolveWindowDefault()(&session());
}
} // namespace ChatHelpers

View File

@@ -0,0 +1,69 @@
/*
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/flags.h"
#include "main/session/session_show.h"
class PhotoData;
class DocumentData;
namespace Data {
struct FileOrigin;
} // namespace Data
namespace Window {
class SessionController;
} // namespace Window
namespace SendMenu {
struct Details;
} // namespace SendMenu
namespace ChatHelpers {
struct FileChosen;
enum class PauseReason {
Any = 0,
InlineResults = (1 << 0),
TabbedPanel = (1 << 1),
Layer = (1 << 2),
RoundPlaying = (1 << 3),
MediaPreview = (1 << 4),
};
using PauseReasons = base::flags<PauseReason>;
inline constexpr bool is_flag_type(PauseReason) { return true; };
using ResolveWindow = Fn<Window::SessionController*(
not_null<Main::Session*>)>;
[[nodiscard]] ResolveWindow ResolveWindowDefault();
class Show : public Main::SessionShow {
public:
virtual void activate() = 0;
[[nodiscard]] virtual bool paused(PauseReason reason) const = 0;
[[nodiscard]] virtual rpl::producer<> pauseChanged() const = 0;
[[nodiscard]] virtual rpl::producer<bool> adjustShadowLeft() const;
[[nodiscard]] virtual SendMenu::Details sendMenuDetails() const = 0;
virtual bool showMediaPreview(
Data::FileOrigin origin,
not_null<DocumentData*> document) const = 0;
virtual bool showMediaPreview(
Data::FileOrigin origin,
not_null<PhotoData*> photo) const = 0;
virtual void processChosenSticker(FileChosen &&chosen) const = 0;
[[nodiscard]] virtual Window::SessionController *resolveWindow() const;
};
} // namespace ChatHelpers

View File

@@ -0,0 +1,493 @@
/*
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/emoji_interactions.h"
#include "chat_helpers/stickers_emoji_pack.h"
#include "history/history_item.h"
#include "history/history.h"
#include "history/view/history_view_element.h"
#include "history/view/media/history_view_sticker.h"
#include "main/main_session.h"
#include "data/data_session.h"
#include "data/data_changes.h"
#include "data/data_peer.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "ui/emoji_config.h"
#include "base/random.h"
#include "apiwrap.h"
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonArray>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonValue>
namespace ChatHelpers {
namespace {
constexpr auto kMinDelay = crl::time(200);
constexpr auto kAccumulateDelay = crl::time(1000);
constexpr auto kAccumulateSeenRequests = kAccumulateDelay;
constexpr auto kAcceptSeenSinceRequest = 3 * crl::time(1000);
constexpr auto kMaxDelay = 2 * crl::time(1000);
constexpr auto kTimeNever = std::numeric_limits<crl::time>::max();
constexpr auto kJsonVersion = 1;
} // namespace
auto EmojiInteractions::Combine(CheckResult a, CheckResult b) -> CheckResult {
return {
.nextCheckAt = std::min(a.nextCheckAt, b.nextCheckAt),
.waitingForDownload = a.waitingForDownload || b.waitingForDownload,
};
}
EmojiInteractions::EmojiInteractions(not_null<Main::Session*> session)
: _session(session)
, _checkTimer([=] { check(); }) {
_session->changes().messageUpdates(
Data::MessageUpdate::Flag::Destroyed
| Data::MessageUpdate::Flag::Edited
) | rpl::on_next([=](const Data::MessageUpdate &update) {
if (update.flags & Data::MessageUpdate::Flag::Destroyed) {
_outgoing.remove(update.item);
_incoming.remove(update.item);
} else if (update.flags & Data::MessageUpdate::Flag::Edited) {
checkEdition(update.item, _outgoing);
checkEdition(update.item, _incoming);
}
}, _lifetime);
}
EmojiInteractions::~EmojiInteractions() = default;
void EmojiInteractions::checkEdition(
not_null<HistoryItem*> item,
base::flat_map<not_null<HistoryItem*>, std::vector<Animation>> &map) {
const auto &pack = _session->emojiStickersPack();
const auto i = map.find(item);
if (i != end(map)
&& (i->second.front().emoji != pack.chooseInteractionEmoji(item))) {
map.erase(i);
}
}
void EmojiInteractions::startOutgoing(
not_null<const HistoryView::Element*> view) {
const auto item = view->data();
if (!item->isRegular() || !item->history()->peer->isUser()) {
return;
}
const auto &pack = _session->emojiStickersPack();
const auto emoticon = item->originalText().text;
const auto emoji = pack.chooseInteractionEmoji(emoticon);
if (!emoji) {
return;
}
const auto &list = pack.animationsForEmoji(emoji);
if (list.empty()) {
return;
}
auto &animations = _outgoing[item];
if (!animations.empty() && animations.front().emoji != emoji) {
// The message was edited, forget the old emoji.
animations.clear();
}
const auto last = !animations.empty() ? &animations.back() : nullptr;
const auto listSize = int(list.size());
const auto chooseDifferent = (last && listSize > 1);
const auto index = chooseDifferent
? base::RandomIndex(listSize - 1)
: base::RandomIndex(listSize);
const auto selected = (begin(list) + index)->second;
const auto document = (chooseDifferent && selected == last->document)
? (begin(list) + index + 1)->second
: selected;
const auto media = document->createMediaView();
media->checkStickerLarge();
const auto now = crl::now();
animations.push_back({
.emoticon = emoticon,
.emoji = emoji,
.document = document,
.media = media,
.scheduledAt = now,
.index = index,
});
check(now);
}
void EmojiInteractions::startIncoming(
not_null<PeerData*> peer,
MsgId messageId,
const QString &emoticon,
EmojiInteractionsBunch &&bunch) {
if (!peer->isUser() || bunch.interactions.empty()) {
return;
}
const auto item = _session->data().message(peer->id, messageId);
if (!item || !item->isRegular()) {
return;
}
const auto &pack = _session->emojiStickersPack();
const auto emoji = pack.chooseInteractionEmoji(item);
if (!emoji || emoji != pack.chooseInteractionEmoji(emoticon)) {
return;
}
const auto &list = pack.animationsForEmoji(emoji);
if (list.empty()) {
return;
}
auto &animations = _incoming[item];
if (!animations.empty() && animations.front().emoji != emoji) {
// The message was edited, forget the old emoji.
animations.clear();
}
const auto now = crl::now();
for (const auto &single : bunch.interactions) {
const auto at = now + crl::time(base::SafeRound(single.time * 1000));
if (!animations.empty() && animations.back().scheduledAt >= at) {
continue;
}
const auto listSize = int(list.size());
const auto index = (single.index - 1);
if (index < listSize) {
const auto document = (begin(list) + index)->second;
const auto media = document->createMediaView();
media->checkStickerLarge();
animations.push_back({
.emoticon = emoticon,
.emoji = emoji,
.document = document,
.media = media,
.scheduledAt = at,
.incoming = true,
.index = index,
});
}
}
if (animations.empty()) {
_incoming.remove(item);
} else {
check(now);
}
}
void EmojiInteractions::seenOutgoing(
not_null<PeerData*> peer,
const QString &emoticon) {
const auto &pack = _session->emojiStickersPack();
if (const auto i = _playsSent.find(peer); i != end(_playsSent)) {
if (const auto emoji = pack.chooseInteractionEmoji(emoticon)) {
if (const auto j = i->second.find(emoji); j != end(i->second)) {
const auto last = j->second.lastDoneReceivedAt;
if (!last || last + kAcceptSeenSinceRequest > crl::now()) {
_seen.fire({ peer, emoticon });
}
}
}
}
}
auto EmojiInteractions::checkAnimations(crl::time now) -> CheckResult {
return Combine(
checkAnimations(now, _outgoing),
checkAnimations(now, _incoming));
}
auto EmojiInteractions::checkAnimations(
crl::time now,
base::flat_map<not_null<HistoryItem*>, std::vector<Animation>> &map
) -> CheckResult {
auto nearest = kTimeNever;
auto waitingForDownload = false;
for (auto i = begin(map); i != end(map);) {
auto lastStartedAt = crl::time();
auto &animations = i->second;
// Erase too old requests.
const auto j = ranges::find_if(animations, [&](const Animation &a) {
return !a.startedAt && (a.scheduledAt + kMaxDelay <= now);
});
if (j == begin(animations)) {
i = map.erase(i);
continue;
} else if (j != end(animations)) {
animations.erase(j, end(animations));
}
const auto item = i->first;
for (auto &animation : animations) {
if (animation.startedAt) {
lastStartedAt = animation.startedAt;
} else if (!animation.media->loaded()) {
animation.media->checkStickerLarge();
waitingForDownload = true;
break;
} else if (!lastStartedAt || lastStartedAt + kMinDelay <= now) {
animation.startedAt = now;
_playRequests.fire({
animation.emoticon,
item,
animation.media,
animation.scheduledAt,
animation.incoming,
});
break;
} else {
nearest = std::min(nearest, lastStartedAt + kMinDelay);
break;
}
}
++i;
}
return {
.nextCheckAt = nearest,
.waitingForDownload = waitingForDownload,
};
}
void EmojiInteractions::sendAccumulatedOutgoing(
crl::time now,
not_null<HistoryItem*> item,
std::vector<Animation> &animations) {
Expects(!animations.empty());
const auto firstStartedAt = animations.front().startedAt;
const auto intervalEnd = firstStartedAt + kAccumulateDelay;
if (intervalEnd > now) {
return;
}
const auto from = begin(animations);
const auto till = ranges::find_if(animations, [&](const auto &animation) {
return !animation.startedAt || (animation.startedAt >= intervalEnd);
});
auto bunch = EmojiInteractionsBunch();
bunch.interactions.reserve(till - from);
for (const auto &animation : ranges::make_subrange(from, till)) {
bunch.interactions.push_back({
.index = animation.index + 1,
.time = (animation.startedAt - firstStartedAt) / 1000.,
});
}
if (bunch.interactions.empty()) {
return;
}
const auto peer = item->history()->peer;
const auto emoji = from->emoji;
const auto requestId = _session->api().request(MTPmessages_SetTyping(
MTP_flags(0),
peer->input(),
MTPint(), // top_msg_id
MTP_sendMessageEmojiInteraction(
MTP_string(from->emoticon),
MTP_int(item->id),
MTP_dataJSON(MTP_bytes(ToJson(bunch))))
)).done([=](const MTPBool &result, mtpRequestId requestId) {
auto &sent = _playsSent[peer][emoji];
if (sent.lastRequestId == requestId) {
sent.lastDoneReceivedAt = crl::now();
if (!_checkTimer.isActive()) {
_checkTimer.callOnce(kAcceptSeenSinceRequest);
}
}
}).send();
_playsSent[peer][emoji] = PlaySent{ .lastRequestId = requestId };
animations.erase(from, till);
}
void EmojiInteractions::clearAccumulatedIncoming(
crl::time now,
std::vector<Animation> &animations) {
Expects(!animations.empty());
const auto from = begin(animations);
const auto till = ranges::find_if(animations, [&](const auto &animation) {
return !animation.startedAt
|| (animation.startedAt + kMinDelay) > now;
});
animations.erase(from, till);
}
auto EmojiInteractions::checkAccumulated(crl::time now) -> CheckResult {
auto nearest = kTimeNever;
for (auto i = begin(_outgoing); i != end(_outgoing);) {
auto &[item, animations] = *i;
sendAccumulatedOutgoing(now, item, animations);
if (animations.empty()) {
i = _outgoing.erase(i);
continue;
} else if (const auto firstStartedAt = animations.front().startedAt) {
nearest = std::min(nearest, firstStartedAt + kAccumulateDelay);
Assert(nearest > now);
}
++i;
}
for (auto i = begin(_incoming); i != end(_incoming);) {
auto &animations = i->second;
clearAccumulatedIncoming(now, animations);
if (animations.empty()) {
i = _incoming.erase(i);
continue;
} else {
// Doesn't really matter when, just clear them finally.
nearest = std::min(nearest, now + kAccumulateDelay);
}
++i;
}
return {
.nextCheckAt = nearest,
};
}
void EmojiInteractions::check(crl::time now) {
if (!now) {
now = crl::now();
}
checkSeenRequests(now);
checkSentRequests(now);
const auto result1 = checkAnimations(now);
const auto result2 = checkAccumulated(now);
const auto result = Combine(result1, result2);
if (result.nextCheckAt < kTimeNever) {
Assert(result.nextCheckAt > now);
_checkTimer.callOnce(result.nextCheckAt - now);
} else if (!_playStarted.empty()) {
_checkTimer.callOnce(kAccumulateSeenRequests);
} else if (!_playsSent.empty()) {
_checkTimer.callOnce(kAcceptSeenSinceRequest);
}
setWaitingForDownload(result.waitingForDownload);
}
void EmojiInteractions::checkSeenRequests(crl::time now) {
for (auto i = begin(_playStarted); i != end(_playStarted);) {
auto &animations = i->second;
for (auto j = begin(animations); j != end(animations);) {
if (j->second + kAccumulateSeenRequests <= now) {
j = animations.erase(j);
} else {
++j;
}
}
if (animations.empty()) {
i = _playStarted.erase(i);
} else {
++i;
}
}
}
void EmojiInteractions::checkSentRequests(crl::time now) {
for (auto i = begin(_playsSent); i != end(_playsSent);) {
auto &animations = i->second;
for (auto j = begin(animations); j != end(animations);) {
const auto last = j->second.lastDoneReceivedAt;
if (last && last + kAcceptSeenSinceRequest <= now) {
j = animations.erase(j);
} else {
++j;
}
}
if (animations.empty()) {
i = _playsSent.erase(i);
} else {
++i;
}
}
}
void EmojiInteractions::setWaitingForDownload(bool waiting) {
if (_waitingForDownload == waiting) {
return;
}
_waitingForDownload = waiting;
if (_waitingForDownload) {
_session->downloaderTaskFinished(
) | rpl::on_next([=] {
check();
}, _downloadCheckLifetime);
} else {
_downloadCheckLifetime.destroy();
_downloadCheckLifetime.destroy();
}
}
void EmojiInteractions::playStarted(not_null<PeerData*> peer, QString emoji) {
auto &map = _playStarted[peer];
const auto i = map.find(emoji);
const auto now = crl::now();
if (i != end(map) && now - i->second < kAccumulateSeenRequests) {
return;
}
_session->api().request(MTPmessages_SetTyping(
MTP_flags(0),
peer->input(),
MTPint(), // top_msg_id
MTP_sendMessageEmojiInteractionSeen(MTP_string(emoji))
)).send();
map[emoji] = now;
if (!_checkTimer.isActive()) {
_checkTimer.callOnce(kAccumulateSeenRequests);
}
}
EmojiInteractionsBunch EmojiInteractions::Parse(const QByteArray &json) {
auto error = QJsonParseError{ 0, QJsonParseError::NoError };
const auto document = QJsonDocument::fromJson(json, &error);
if (error.error != QJsonParseError::NoError || !document.isObject()) {
LOG(("API Error: Bad interactions json received."));
return {};
}
const auto root = document.object();
const auto version = root.value("v").toInt();
if (version != kJsonVersion) {
LOG(("API Error: Bad interactions version: %1").arg(version));
return {};
}
const auto actions = root.value("a").toArray();
if (actions.empty()) {
LOG(("API Error: Empty interactions list."));
return {};
}
auto result = EmojiInteractionsBunch();
for (const auto interaction : actions) {
const auto object = interaction.toObject();
const auto index = object.value("i").toInt();
if (index < 0 || index > 10) {
LOG(("API Error: Bad interaction index: %1").arg(index));
return {};
}
const auto time = object.value("t").toDouble();
if (time < 0.
|| time > 1.
|| (!result.interactions.empty()
&& time <= result.interactions.back().time)) {
LOG(("API Error: Bad interaction time: %1").arg(time));
continue;
}
result.interactions.push_back({ .index = index, .time = time });
}
return result;
}
QByteArray EmojiInteractions::ToJson(const EmojiInteractionsBunch &bunch) {
auto list = QJsonArray();
for (const auto &single : bunch.interactions) {
list.push_back(QJsonObject{
{ "i", single.index },
{ "t", single.time },
});
}
return QJsonDocument(QJsonObject{
{ "v", kJsonVersion },
{ "a", std::move(list) },
}).toJson(QJsonDocument::Compact);
}
} // namespace ChatHelpers

View File

@@ -0,0 +1,142 @@
/*
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/timer.h"
class PeerData;
class HistoryItem;
class DocumentData;
namespace Data {
class DocumentMedia;
} // namespace Data
namespace Main {
class Session;
} // namespace Main
namespace HistoryView {
class Element;
} // namespace HistoryView
namespace ChatHelpers {
struct EmojiInteractionPlayRequest {
QString emoticon;
not_null<HistoryItem*> item;
std::shared_ptr<Data::DocumentMedia> media;
crl::time shouldHaveStartedAt = 0;
bool incoming = false;
};
struct EmojiInteractionsBunch {
struct Single {
int index = 0;
double time = 0;
};
std::vector<Single> interactions;
};
struct EmojiInteractionSeen {
not_null<PeerData*> peer;
QString emoticon;
};
class EmojiInteractions final {
public:
explicit EmojiInteractions(not_null<Main::Session*> session);
~EmojiInteractions();
using PlayRequest = EmojiInteractionPlayRequest;
void startOutgoing(not_null<const HistoryView::Element*> view);
void startIncoming(
not_null<PeerData*> peer,
MsgId messageId,
const QString &emoticon,
EmojiInteractionsBunch &&bunch);
void seenOutgoing(not_null<PeerData*> peer, const QString &emoticon);
[[nodiscard]] rpl::producer<EmojiInteractionSeen> seen() const {
return _seen.events();
}
[[nodiscard]] rpl::producer<PlayRequest> playRequests() const {
return _playRequests.events();
}
void playStarted(not_null<PeerData*> peer, QString emoji);
[[nodiscard]] static EmojiInteractionsBunch Parse(const QByteArray &json);
[[nodiscard]] static QByteArray ToJson(
const EmojiInteractionsBunch &bunch);
private:
struct Animation {
QString emoticon;
not_null<EmojiPtr> emoji;
not_null<DocumentData*> document;
std::shared_ptr<Data::DocumentMedia> media;
crl::time scheduledAt = 0;
crl::time startedAt = 0;
bool incoming = false;
int index = 0;
};
struct PlaySent {
mtpRequestId lastRequestId = 0;
crl::time lastDoneReceivedAt = 0;
};
struct CheckResult {
crl::time nextCheckAt = 0;
bool waitingForDownload = false;
};
[[nodiscard]] static CheckResult Combine(CheckResult a, CheckResult b);
void check(crl::time now = 0);
[[nodiscard]] CheckResult checkAnimations(crl::time now);
[[nodiscard]] CheckResult checkAnimations(
crl::time now,
base::flat_map<not_null<HistoryItem*>, std::vector<Animation>> &map);
[[nodiscard]] CheckResult checkAccumulated(crl::time now);
void sendAccumulatedOutgoing(
crl::time now,
not_null<HistoryItem*> item,
std::vector<Animation> &animations);
void clearAccumulatedIncoming(
crl::time now,
std::vector<Animation> &animations);
void setWaitingForDownload(bool waiting);
void checkSeenRequests(crl::time now);
void checkSentRequests(crl::time now);
void checkEdition(
not_null<HistoryItem*> item,
base::flat_map<not_null<HistoryItem*>, std::vector<Animation>> &map);
const not_null<Main::Session*> _session;
base::flat_map<not_null<HistoryItem*>, std::vector<Animation>> _outgoing;
base::flat_map<not_null<HistoryItem*>, std::vector<Animation>> _incoming;
base::Timer _checkTimer;
rpl::event_stream<PlayRequest> _playRequests;
base::flat_map<
not_null<PeerData*>,
base::flat_map<QString, crl::time>> _playStarted;
base::flat_map<
not_null<PeerData*>,
base::flat_map<not_null<EmojiPtr>, PlaySent>> _playsSent;
rpl::event_stream<EmojiInteractionSeen> _seen;
bool _waitingForDownload = false;
rpl::lifetime _downloadCheckLifetime;
rpl::lifetime _lifetime;
};
} // namespace ChatHelpers

View File

@@ -0,0 +1,759 @@
/*
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/emoji_keywords.h"
#include "emoji_suggestions_helper.h"
#include "lang/lang_instance.h"
#include "lang/lang_cloud_manager.h"
#include "lang/lang_keys.h"
#include "core/application.h"
#include "base/platform/base_platform_info.h"
#include "ui/emoji_config.h"
#include "main/main_domain.h"
#include "main/main_session.h"
#include "apiwrap.h"
#include "core/application.h"
#include "core/core_settings.h"
#include <QtGui/QGuiApplication>
namespace ChatHelpers {
namespace {
constexpr auto kRefreshEach = 60 * 60 * crl::time(1000); // 1 hour.
constexpr auto kKeepNotUsedLangPacksCount = 4;
constexpr auto kKeepNotUsedInputLanguagesCount = 4;
using namespace Ui::Emoji;
using Result = EmojiKeywords::Result;
struct LangPackEmoji {
EmojiPtr emoji = nullptr;
QString text;
};
struct LangPackData {
int version = 0;
int maxKeyLength = 0;
std::map<QString, std::vector<LangPackEmoji>> emoji;
};
[[nodiscard]] bool MustAddPostfix(const QString &text) {
if (text.size() != 1) {
return false;
}
const auto code = text[0].unicode();
return (code == 0x2122U) || (code == 0xA9U) || (code == 0xAEU);
}
[[nodiscard]] bool SkipExactKeyword(
const QString &language,
const QString &word) {
if ((word.size() == 1) && !word[0].isLetter()) {
return true;
} else if (word == u"10"_q) {
return true;
} else if (language != u"en"_q) {
return false;
} else if ((word.size() == 1)
&& (word[0] != '$')
&& (word[0].unicode() != 8364)) { // Euro.
return true;
} else if ((word.size() == 2)
&& (word != u"us"_q)
&& (word != u"uk"_q)
&& (word != u"hi"_q)
&& (word != u"ok"_q)) {
return true;
}
return false;
}
[[nodiscard]] EmojiPtr FindExact(const QString &text) {
auto length = 0;
const auto result = Find(text, &length);
return (length < text.size()) ? nullptr : result;
}
void CreateCacheFilePath() {
QDir().mkpath(internal::CacheFileFolder() + u"/keywords"_q);
}
[[nodiscard]] QString CacheFilePath(QString id) {
static const auto BadSymbols = QRegularExpression("[^a-zA-Z0-9_\\.\\-]");
id.replace(BadSymbols, QString());
if (id.isEmpty()) {
return QString();
}
return internal::CacheFileFolder() + u"/keywords/"_q + id;
}
[[nodiscard]] LangPackData ReadLocalCache(const QString &id) {
auto file = QFile(CacheFilePath(id));
if (!file.open(QIODevice::ReadOnly)) {
return {};
}
auto result = LangPackData();
auto stream = QDataStream(&file);
stream.setVersion(QDataStream::Qt_5_1);
auto version = qint32();
auto count = qint32();
stream
>> version
>> count;
if (version < 0 || count < 0 || stream.status() != QDataStream::Ok) {
return {};
}
for (auto i = 0; i != count; ++i) {
auto key = QString();
auto size = qint32();
stream
>> key
>> size;
if (size < 0 || stream.status() != QDataStream::Ok) {
return {};
}
auto &list = result.emoji[key];
for (auto j = 0; j != size; ++j) {
auto text = QString();
stream >> text;
if (stream.status() != QDataStream::Ok) {
return {};
}
const auto emoji = MustAddPostfix(text)
? (text + QChar(Ui::Emoji::kPostfix))
: text;
const auto entry = LangPackEmoji{ FindExact(emoji), text };
if (!entry.emoji) {
return {};
}
list.push_back(entry);
}
result.maxKeyLength = std::max(result.maxKeyLength, int(key.size()));
}
result.version = version;
return result;
}
void WriteLocalCache(const QString &id, const LangPackData &data) {
if (!data.version && data.emoji.empty()) {
return;
}
CreateCacheFilePath();
auto file = QFile(CacheFilePath(id));
if (!file.open(QIODevice::WriteOnly)) {
return;
}
auto stream = QDataStream(&file);
stream.setVersion(QDataStream::Qt_5_1);
stream
<< qint32(data.version)
<< qint32(data.emoji.size());
for (const auto &[key, list] : data.emoji) {
stream
<< key
<< qint32(list.size());
for (const auto &emoji : list) {
stream << emoji.text;
}
}
}
[[nodiscard]] QString NormalizeQuery(const QString &query) {
return query.toLower();
}
[[nodiscard]] QString NormalizeKey(const QString &key) {
return key.toLower().trimmed();
}
void AppendFoundEmoji(
std::vector<Result> &result,
const QString &label,
const std::vector<LangPackEmoji> &list) {
// It is important that the 'result' won't relocate while inserting.
result.reserve(result.size() + list.size());
const auto alreadyBegin = begin(result);
const auto alreadyEnd = alreadyBegin + result.size();
auto &&add = ranges::views::all(
list
) | ranges::views::filter([&](const LangPackEmoji &entry) {
const auto i = ranges::find(
alreadyBegin,
alreadyEnd,
entry.emoji,
&Result::emoji);
return (i == alreadyEnd);
}) | ranges::views::transform([&](const LangPackEmoji &entry) {
return Result{ entry.emoji, label, entry.text };
});
result.insert(end(result), add.begin(), add.end());
}
void AppendLegacySuggestions(
std::vector<Result> &result,
const QString &query) {
const auto badSuggestionChar = [](QChar ch) {
return (ch < 'a' || ch > 'z')
&& (ch < 'A' || ch > 'Z')
&& (ch < '0' || ch > '9')
&& (ch != '_')
&& (ch != '-')
&& (ch != '+');
};
if (ranges::any_of(query, badSuggestionChar)) {
return;
}
const auto suggestions = GetSuggestions(QStringToUTF16(query));
// It is important that the 'result' won't relocate while inserting.
result.reserve(result.size() + suggestions.size());
const auto alreadyBegin = begin(result);
const auto alreadyEnd = alreadyBegin + result.size();
auto &&add = ranges::views::all(
suggestions
) | ranges::views::transform([](const Suggestion &suggestion) {
return Result{
Find(QStringFromUTF16(suggestion.emoji())),
QStringFromUTF16(suggestion.label()),
QStringFromUTF16(suggestion.replacement())
};
}) | ranges::views::filter([&](const Result &entry) {
const auto i = entry.emoji
? ranges::find(
alreadyBegin,
alreadyEnd,
entry.emoji,
&Result::emoji)
: alreadyEnd;
return (entry.emoji != nullptr)
&& (i == alreadyEnd);
});
result.insert(end(result), add.begin(), add.end());
}
void ApplyDifference(
LangPackData &data,
const QVector<MTPEmojiKeyword> &keywords,
int version) {
data.version = version;
for (const auto &keyword : keywords) {
keyword.match([&](const MTPDemojiKeyword &keyword) {
const auto word = NormalizeKey(qs(keyword.vkeyword()));
if (word.isEmpty()) {
return;
}
auto &list = data.emoji[word];
auto &&emoji = ranges::views::all(
keyword.vemoticons().v
) | ranges::views::transform([](const MTPstring &string) {
const auto text = qs(string);
const auto emoji = MustAddPostfix(text)
? (text + QChar(Ui::Emoji::kPostfix))
: text;
return LangPackEmoji{ FindExact(emoji), text };
}) | ranges::views::filter([&](const LangPackEmoji &entry) {
if (!entry.emoji) {
LOG(("API Warning: emoji %1 is not supported, word: %2."
).arg(
entry.text,
word));
}
return (entry.emoji != nullptr);
});
list.insert(end(list), emoji.begin(), emoji.end());
}, [&](const MTPDemojiKeywordDeleted &keyword) {
const auto word = NormalizeKey(qs(keyword.vkeyword()));
if (word.isEmpty()) {
return;
}
const auto i = data.emoji.find(word);
if (i == end(data.emoji)) {
return;
}
auto &list = i->second;
for (const auto &emoji : keyword.vemoticons().v) {
list.erase(
ranges::remove(list, qs(emoji), &LangPackEmoji::text),
end(list));
}
if (list.empty()) {
data.emoji.erase(i);
}
});
}
if (data.emoji.empty()) {
data.maxKeyLength = 0;
} else {
auto &&lengths = ranges::views::all(
data.emoji
) | ranges::views::transform([](auto &&pair) {
return pair.first.size();
});
data.maxKeyLength = *ranges::max_element(lengths);
}
}
} // namespace
class EmojiKeywords::LangPack final {
public:
using Delegate = details::EmojiKeywordsLangPackDelegate;
LangPack(not_null<Delegate*> delegate, const QString &id);
LangPack(const LangPack &other) = delete;
LangPack &operator=(const LangPack &other) = delete;
~LangPack();
[[nodiscard]] QString id() const;
void refresh();
void apiChanged();
[[nodiscard]] std::vector<Result> query(
const QString &normalized,
bool exact) const;
[[nodiscard]] int maxQueryLength() const;
private:
enum class State {
ReadingCache,
PendingRequest,
Requested,
Refreshed,
};
void readLocalCache();
void applyDifference(const MTPEmojiKeywordsDifference &result);
void applyData(LangPackData &&data);
not_null<Delegate*> _delegate;
QString _id;
State _state = State::ReadingCache;
LangPackData _data;
crl::time _lastRefreshTime = 0;
mtpRequestId _requestId = 0;
base::binary_guard _guard;
};
EmojiKeywords::LangPack::LangPack(
not_null<Delegate*> delegate,
const QString &id)
: _delegate(delegate)
, _id(id) {
readLocalCache();
}
EmojiKeywords::LangPack::~LangPack() {
if (_requestId) {
if (const auto api = _delegate->api()) {
api->request(_requestId).cancel();
}
}
}
void EmojiKeywords::LangPack::readLocalCache() {
const auto id = _id;
auto callback = crl::guard(_guard.make_guard(), [=](
LangPackData &&result) {
applyData(std::move(result));
refresh();
});
crl::async([id, callback = std::move(callback)]() mutable {
crl::on_main([
callback = std::move(callback),
result = ReadLocalCache(id)
]() mutable {
callback(std::move(result));
});
});
}
QString EmojiKeywords::LangPack::id() const {
return _id;
}
void EmojiKeywords::LangPack::refresh() {
if (_state != State::Refreshed) {
return;
} else if (_lastRefreshTime > 0
&& crl::now() - _lastRefreshTime < kRefreshEach) {
return;
}
const auto api = _delegate->api();
if (!api) {
_state = State::PendingRequest;
return;
}
_state = State::Requested;
const auto send = [&](auto &&request) {
return api->request(
std::move(request)
).done([=](const MTPEmojiKeywordsDifference &result) {
_requestId = 0;
_lastRefreshTime = crl::now();
applyDifference(result);
}).fail([=] {
_requestId = 0;
_lastRefreshTime = crl::now();
}).send();
};
_requestId = (_data.version > 0)
? send(MTPmessages_GetEmojiKeywordsDifference(
MTP_string(_id),
MTP_int(_data.version)))
: send(MTPmessages_GetEmojiKeywords(
MTP_string(_id)));
}
void EmojiKeywords::LangPack::applyDifference(
const MTPEmojiKeywordsDifference &result) {
result.match([&](const MTPDemojiKeywordsDifference &data) {
const auto code = qs(data.vlang_code());
const auto version = data.vversion().v;
const auto &keywords = data.vkeywords().v;
if (code != _id) {
LOG(("API Error: Bad lang_code for emoji keywords %1 -> %2").arg(
_id,
code));
_data.version = 0;
_state = State::Refreshed;
return;
} else if (keywords.isEmpty() && _data.version >= version) {
_state = State::Refreshed;
return;
}
const auto id = _id;
auto copy = _data;
auto callback = crl::guard(_guard.make_guard(), [=](
LangPackData &&result) {
applyData(std::move(result));
});
crl::async([=,
copy = std::move(copy),
callback = std::move(callback)]() mutable {
ApplyDifference(copy, keywords, version);
WriteLocalCache(id, copy);
crl::on_main([
result = std::move(copy),
callback = std::move(callback)
]() mutable {
callback(std::move(result));
});
});
});
}
void EmojiKeywords::LangPack::applyData(LangPackData &&data) {
_data = std::move(data);
_state = State::Refreshed;
_delegate->langPackRefreshed();
}
void EmojiKeywords::LangPack::apiChanged() {
if (_state == State::Requested && !_delegate->api()) {
_requestId = 0;
} else if (_state != State::PendingRequest) {
return;
}
_state = State::Refreshed;
refresh();
}
std::vector<Result> EmojiKeywords::LangPack::query(
const QString &normalized,
bool exact) const {
if (normalized.size() > _data.maxKeyLength
|| _data.emoji.empty()
|| (exact && SkipExactKeyword(_id, normalized))) {
return {};
}
const auto from = _data.emoji.lower_bound(normalized);
auto &&chosen = ranges::make_subrange(
from,
end(_data.emoji)
) | ranges::views::take_while([&](const auto &pair) {
const auto &key = pair.first;
return exact ? (key == normalized) : key.startsWith(normalized);
});
auto result = std::vector<Result>();
for (const auto &[key, list] : chosen) {
AppendFoundEmoji(result, key, list);
}
return result;
}
int EmojiKeywords::LangPack::maxQueryLength() const {
return _data.maxKeyLength;
}
EmojiKeywords::EmojiKeywords() {
crl::on_main(&_guard, [=] {
handleSessionChanges();
});
}
EmojiKeywords::~EmojiKeywords() = default;
not_null<details::EmojiKeywordsLangPackDelegate*> EmojiKeywords::delegate() {
return static_cast<details::EmojiKeywordsLangPackDelegate*>(this);
}
ApiWrap *EmojiKeywords::api() {
return _api;
}
void EmojiKeywords::langPackRefreshed() {
_refreshed.fire({});
}
void EmojiKeywords::handleSessionChanges() {
Core::App().domain().activeSessionValue( // #TODO multi someSessionValue
) | rpl::map([](Main::Session *session) {
return session ? &session->api() : nullptr;
}) | rpl::on_next([=](ApiWrap *api) {
apiChanged(api);
}, _lifetime);
}
void EmojiKeywords::apiChanged(ApiWrap *api) {
_api = api;
if (_api) {
crl::on_main(&_api->session(), crl::guard(&_guard, [=] {
Lang::CurrentCloudManager().firstLanguageSuggestion(
) | rpl::filter([=] {
// Refresh with the suggested language if we already were asked.
return !_data.empty();
}) | rpl::on_next([=] {
refresh();
}, _suggestedChangeLifetime);
}));
} else {
_langsRequestId = 0;
_suggestedChangeLifetime.destroy();
}
for (const auto &[language, item] : _data) {
item->apiChanged();
}
}
void EmojiKeywords::refresh() {
auto list = languages();
if (_localList != list) {
_localList = std::move(list);
refreshRemoteList();
} else {
refreshFromRemoteList();
}
}
std::vector<QString> EmojiKeywords::languages() {
if (!_api) {
return {};
}
refreshInputLanguages();
auto result = std::vector<QString>();
const auto yield = [&](const QString &language) {
result.push_back(language);
};
const auto yieldList = [&](const QStringList &list) {
result.insert(end(result), list.begin(), list.end());
};
yield(Lang::Id());
yield(Lang::DefaultLanguageId());
yield(Lang::CurrentCloudManager().suggestedLanguage());
yield(Platform::SystemLanguage());
yieldList(QLocale::system().uiLanguages());
for (const auto &list : _inputLanguages) {
yieldList(list);
}
ranges::sort(result);
return result;
}
void EmojiKeywords::refreshInputLanguages() {
const auto method = QGuiApplication::inputMethod();
if (!method) {
return;
}
const auto list = method->locale().uiLanguages();
const auto i = ranges::find(_inputLanguages, list);
if (i != end(_inputLanguages)) {
std::rotate(i, i + 1, end(_inputLanguages));
} else {
if (_inputLanguages.size() >= kKeepNotUsedInputLanguagesCount) {
_inputLanguages.pop_front();
}
_inputLanguages.push_back(list);
}
}
rpl::producer<> EmojiKeywords::refreshed() const {
return _refreshed.events();
}
std::vector<Result> EmojiKeywords::query(
const QString &query,
bool exact) const {
const auto normalized = NormalizeQuery(query);
if (normalized.isEmpty()) {
return {};
}
auto result = std::vector<Result>();
for (const auto &[language, item] : _data) {
const auto list = item->query(normalized, exact);
// It is important that the 'result' won't relocate while inserting.
result.reserve(result.size() + list.size());
const auto alreadyBegin = begin(result);
const auto alreadyEnd = alreadyBegin + result.size();
auto &&add = ranges::views::all(
list
) | ranges::views::filter([&](Result entry) {
// In each item->query() result the list has no duplicates.
// So we need to check only for duplicates between queries.
const auto i = ranges::find(
alreadyBegin,
alreadyEnd,
entry.emoji,
&Result::emoji);
return (i == alreadyEnd);
});
result.insert(end(result), add.begin(), add.end());
}
if (!exact) {
AppendLegacySuggestions(result, query);
}
return result;
}
std::vector<Result> EmojiKeywords::queryMine(
const QString &query,
bool exact) const {
return ApplyVariants(PrioritizeRecent(this->query(query, exact)));
}
std::vector<Result> EmojiKeywords::PrioritizeRecent(
std::vector<Result> list) {
using Entry = Result;
auto lastRecent = begin(list);
const auto &recent = Core::App().settings().recentEmoji();
for (const auto &item : recent) {
const auto emoji = std::get_if<EmojiPtr>(&item.id.data);
if (!emoji) {
continue;
}
const auto original = (*emoji)->original()
? (*emoji)->original()
: (*emoji);
const auto it = ranges::find(list, original, [](const Entry &entry) {
return entry.emoji;
});
if (it > lastRecent && it != end(list)) {
std::rotate(lastRecent, it, it + 1);
++lastRecent;
}
}
return list;
}
std::vector<Result> EmojiKeywords::ApplyVariants(std::vector<Result> list) {
auto &settings = Core::App().settings();
for (auto &item : list) {
item.emoji = settings.lookupEmojiVariant(item.emoji);
}
return list;
}
int EmojiKeywords::maxQueryLength() const {
if (_data.empty()) {
return 0;
}
auto &&lengths = _data | ranges::views::transform([](const auto &pair) {
return pair.second->maxQueryLength();
});
return *ranges::max_element(lengths);
}
void EmojiKeywords::refreshRemoteList() {
if (!_api) {
_localList.clear();
setRemoteList({});
return;
}
_api->request(base::take(_langsRequestId)).cancel();
auto languages = QVector<MTPstring>();
for (const auto &id : _localList) {
languages.push_back(MTP_string(id));
}
_langsRequestId = _api->request(MTPmessages_GetEmojiKeywordsLanguages(
MTP_vector<MTPstring>(languages)
)).done([=](const MTPVector<MTPEmojiLanguage> &result) {
setRemoteList(ranges::views::all(
result.v
) | ranges::views::transform([](const MTPEmojiLanguage &language) {
return language.match([&](const MTPDemojiLanguage &language) {
return qs(language.vlang_code());
});
}) | ranges::to_vector);
_langsRequestId = 0;
}).fail([=] {
_langsRequestId = 0;
}).send();
}
void EmojiKeywords::setRemoteList(std::vector<QString> &&list) {
if (_remoteList == list) {
return;
}
_remoteList = std::move(list);
for (auto i = begin(_data); i != end(_data);) {
if (ranges::find(_remoteList, i->first) != end(_remoteList)) {
++i;
} else {
if (_notUsedData.size() >= kKeepNotUsedLangPacksCount) {
_notUsedData.pop_front();
}
_notUsedData.push_back(std::move(i->second));
i = _data.erase(i);
}
}
refreshFromRemoteList();
}
void EmojiKeywords::refreshFromRemoteList() {
for (const auto &id : _remoteList) {
if (const auto i = _data.find(id); i != end(_data)) {
i->second->refresh();
continue;
}
const auto i = ranges::find(
_notUsedData,
id,
[](const std::unique_ptr<LangPack> &p) { return p->id(); });
if (i != end(_notUsedData)) {
_data.emplace(id, std::move(*i));
_notUsedData.erase(i);
} else {
_data.emplace(
id,
std::make_unique<LangPack>(delegate(), id));
}
}
}
} // namespace ChatHelpers

View File

@@ -0,0 +1,87 @@
/*
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 ApiWrap;
namespace ChatHelpers {
namespace details {
class EmojiKeywordsLangPackDelegate {
public:
virtual ApiWrap *api() = 0;
virtual void langPackRefreshed() = 0;
protected:
~EmojiKeywordsLangPackDelegate() = default;
};
} // namespace details
class EmojiKeywords final : private details::EmojiKeywordsLangPackDelegate {
public:
EmojiKeywords();
EmojiKeywords(const EmojiKeywords &other) = delete;
EmojiKeywords &operator=(const EmojiKeywords &other) = delete;
~EmojiKeywords();
void refresh();
[[nodiscard]] rpl::producer<> refreshed() const;
struct Result {
EmojiPtr emoji = nullptr;
QString label;
QString replacement;
};
[[nodiscard]] std::vector<Result> query(
const QString &query,
bool exact = false) const;
[[nodiscard]] std::vector<Result> queryMine(
const QString &query,
bool exact = false) const;
[[nodiscard]] int maxQueryLength() const;
private:
class LangPack;
not_null<details::EmojiKeywordsLangPackDelegate*> delegate();
ApiWrap *api() override;
void langPackRefreshed() override;
[[nodiscard]] static std::vector<Result> PrioritizeRecent(
std::vector<Result> list);
[[nodiscard]] static std::vector<Result> ApplyVariants(
std::vector<Result> list);
void handleSessionChanges();
void apiChanged(ApiWrap *api);
void refreshInputLanguages();
[[nodiscard]] std::vector<QString> languages();
void refreshRemoteList();
void setRemoteList(std::vector<QString> &&list);
void refreshFromRemoteList();
ApiWrap *_api = nullptr;
std::vector<QString> _localList;
std::vector<QString> _remoteList;
mtpRequestId _langsRequestId = 0;
base::flat_map<QString, std::unique_ptr<LangPack>> _data;
std::deque<std::unique_ptr<LangPack>> _notUsedData;
std::deque<QStringList> _inputLanguages;
rpl::event_stream<> _refreshed;
rpl::lifetime _suggestedChangeLifetime;
rpl::lifetime _lifetime;
base::has_weak_ptr _guard;
};
} // namespace ChatHelpers

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,500 @@
/*
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 "chat_helpers/compose/compose_features.h"
#include "chat_helpers/tabbed_selector.h"
#include "ui/widgets/tooltip.h"
#include "ui/round_rect.h"
#include "base/timer.h"
class StickerPremiumMark;
namespace style {
struct EmojiPan;
} // namespace style
namespace Core {
struct RecentEmojiId;
} // namespace Core
namespace Data {
class StickersSet;
} // namespace Data
namespace PowerSaving {
enum Flag : uint32;
} // namespace PowerSaving
namespace tr {
template <typename ...Tags>
struct phrase;
} // namespace tr
namespace Ui {
class RippleAnimation;
class TabbedSearch;
} // namespace Ui
namespace Ui::Emoji {
enum class Section;
} // namespace Ui::Emoji
namespace Ui::Text {
class CustomEmoji;
struct CustomEmojiPaintContext;
} // namespace Ui::Text
namespace Ui::CustomEmoji {
class Loader;
class Instance;
struct RepaintRequest;
} // namespace Ui::CustomEmoji
namespace Window {
class SessionController;
class MediaPreviewWidget;
} // namespace Window
namespace ChatHelpers {
inline constexpr auto kEmojiSectionCount = 8;
struct StickerIcon;
class EmojiColorPicker;
class StickersListFooter;
class GradientPremiumStar;
class LocalStickersManager;
enum class EmojiListMode {
Full,
TopicIcon,
EmojiStatus,
ChannelStatus,
FullReactions,
RecentReactions,
UserpicBuilder,
BackgroundEmoji,
PeerTitle,
MessageEffects,
};
[[nodiscard]] std::vector<EmojiStatusId> DocumentListToRecent(
const std::vector<DocumentId> &documents);
struct EmojiListDescriptor {
std::shared_ptr<Show> show;
EmojiListMode mode = EmojiListMode::Full;
Fn<QColor()> customTextColor;
Fn<bool()> paused;
std::vector<EmojiStatusId> customRecentList;
Fn<std::unique_ptr<Ui::Text::CustomEmoji>(
DocumentId,
Fn<void()>)> customRecentFactory;
base::flat_set<DocumentId> freeEffects;
const style::EmojiPan *st = nullptr;
ComposeFeatures features;
QWidget *mediaPreviewParent = nullptr;
QMargins mediaPreviewMargins;
};
class EmojiListWidget final
: public TabbedSelector::Inner
, public Ui::AbstractTooltipShower {
public:
using Mode = EmojiListMode;
EmojiListWidget(
QWidget *parent,
not_null<Window::SessionController*> controller,
PauseReason level,
Mode mode);
EmojiListWidget(QWidget *parent, EmojiListDescriptor &&descriptor);
~EmojiListWidget();
using Section = Ui::Emoji::Section;
void refreshRecent() override;
void clearSelection() override;
object_ptr<TabbedSelector::InnerFooter> createFooter() override;
void afterShown() override;
void beforeHiding() override;
void showSet(uint64 setId);
[[nodiscard]] uint64 currentSet(int yOffset) const;
void setAllowWithoutPremium(bool allow);
void showMegagroupSet(ChannelData *megagroup);
// Ui::AbstractTooltipShower interface.
QString tooltipText() const override;
QPoint tooltipPos() const override;
bool tooltipWindowActive() const override;
void refreshEmoji();
[[nodiscard]] rpl::producer<EmojiChosen> chosen() const;
[[nodiscard]] rpl::producer<FileChosen> customChosen() const;
[[nodiscard]] rpl::producer<> jumpedToPremium() const;
[[nodiscard]] rpl::producer<> escapes() const;
void provideRecent(const std::vector<EmojiStatusId> &customRecentList);
void prepareExpanding();
void paintExpanding(
Painter &p,
QRect clip,
int finalBottom,
float64 geometryProgress,
float64 fullProgress,
RectPart origin);
base::unique_qptr<Ui::PopupMenu> fillContextMenu(
const SendMenu::Details &details) override;
[[nodiscard]] rpl::producer<std::vector<QString>> searchQueries() const;
[[nodiscard]] rpl::producer<int> recentShownCount() const;
protected:
void visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void paintEvent(QPaintEvent *e) override;
void leaveEventHook(QEvent *e) override;
void leaveToChildEvent(QEvent *e, QWidget *child) override;
void enterFromChildEvent(QEvent *e, QWidget *child) override;
bool eventHook(QEvent *e) override;
TabbedSelector::InnerFooter *getFooter() const override;
void processHideFinished() override;
void processPanelHideFinished() override;
int countDesiredHeight(int newWidth) override;
int defaultMinimalHeight() const override;
private:
struct SectionInfo {
int section = 0;
int count = 0;
int top = 0;
int rowsCount = 0;
int rowsTop = 0;
int rowsBottom = 0;
bool premiumRequired = false;
bool collapsed = false;
};
struct CustomOne {
std::shared_ptr<Data::EmojiStatusCollectible> collectible;
not_null<Ui::Text::CustomEmoji*> custom;
not_null<DocumentData*> document;
EmojiPtr emoji = nullptr;
};
struct CustomSet {
uint64 id = 0;
not_null<Data::StickersSet*> set;
DocumentData *thumbnailDocument = nullptr;
QString title;
std::vector<CustomOne> list;
mutable std::unique_ptr<Ui::RippleAnimation> ripple;
bool painted = false;
bool expanded = false;
bool canRemove = false;
bool premiumRequired = false;
};
struct CustomEmojiInstance;
struct RightButton {
QImage back;
QImage backOver;
QImage rippleMask;
QString text;
int textWidth = 0;
};
struct RecentOne;
struct OverEmoji {
int section = 0;
int index = 0;
inline bool operator==(OverEmoji other) const {
return (section == other.section)
&& (index == other.index);
}
inline bool operator!=(OverEmoji other) const {
return !(*this == other);
}
};
struct OverSet {
int section = 0;
inline bool operator==(OverSet other) const {
return (section == other.section);
}
inline bool operator!=(OverSet other) const {
return !(*this == other);
}
};
struct OverButton {
int section = 0;
inline bool operator==(OverButton other) const {
return (section == other.section);
}
inline bool operator!=(OverButton other) const {
return !(*this == other);
}
};
using OverState = std::variant<
v::null_t,
OverEmoji,
OverSet,
OverButton>;
struct ExpandingContext {
float64 progress = 0.;
int finalHeight = 0;
bool expanding = false;
};
struct ResolvedCustom {
DocumentData *document = nullptr;
std::shared_ptr<Data::EmojiStatusCollectible> collectible;
explicit operator bool() const {
return document != nullptr;
}
};
template <typename Callback>
bool enumerateSections(Callback callback) const;
[[nodiscard]] SectionInfo sectionInfo(int section) const;
[[nodiscard]] SectionInfo sectionInfoByOffset(int yOffset) const;
[[nodiscard]] int sectionsCount() const;
void setSingleSize(QSize size);
void setColorAllForceRippled(bool force);
void showPicker();
void pickerHidden();
void colorChosen(EmojiChosen data);
bool checkPickerHide();
void refreshCustom();
enum class GroupStickersPlace {
Visible,
Hidden,
};
void refreshEmojiStatusCollectibles();
void refreshMegagroupStickers(
Fn<void(uint64 setId, bool installed)> push,
GroupStickersPlace place);
void unloadNotSeenCustom(int visibleTop, int visibleBottom);
void unloadAllCustom();
void unloadCustomIn(const SectionInfo &info);
void setupSearch();
[[nodiscard]] std::vector<EmojiPtr> collectPlainSearchResults();
void appendPremiumSearchResults();
void ensureLoaded(int section);
void updateSelected();
void setSelected(OverState newSelected);
void setPressed(OverState newPressed);
void fillRecentMenu(
not_null<Ui::PopupMenu*> menu,
int section,
int index);
void fillEmojiStatusMenu(
not_null<Ui::PopupMenu*> menu,
int section,
int index);
[[nodiscard]] EmojiPtr lookupOverEmoji(const OverEmoji *over) const;
[[nodiscard]] ResolvedCustom lookupCustomEmoji(
const OverEmoji *over) const;
[[nodiscard]] ResolvedCustom lookupCustomEmoji(
int index,
int section) const;
[[nodiscard]] EmojiChosen lookupChosen(
EmojiPtr emoji,
not_null<const OverEmoji*> over);
[[nodiscard]] FileChosen lookupChosen(
ResolvedCustom custom,
const OverEmoji *over,
Api::SendOptions options = Api::SendOptions());
void selectEmoji(EmojiChosen data);
void selectCustom(FileChosen data);
void paint(Painter &p, ExpandingContext context, QRect clip);
void drawCollapsedBadge(QPainter &p, QPoint position, int count);
void drawRecent(
QPainter &p,
const ExpandingContext &context,
QPoint position,
const RecentOne &recent);
void drawEmoji(
QPainter &p,
const ExpandingContext &context,
QPoint position,
EmojiPtr emoji);
void drawCustom(
QPainter &p,
const ExpandingContext &context,
QPoint position,
int set,
int index);
void validateEmojiPaintContext(const ExpandingContext &context);
[[nodiscard]] bool hasColorButton(int index) const;
[[nodiscard]] QRect colorButtonRect(int index) const;
[[nodiscard]] QRect colorButtonRect(const SectionInfo &info) const;
[[nodiscard]] bool hasRemoveButton(int index) const;
[[nodiscard]] QRect removeButtonRect(int index) const;
[[nodiscard]] QRect removeButtonRect(const SectionInfo &info) const;
[[nodiscard]] bool hasAddButton(int index) const;
[[nodiscard]] QRect addButtonRect(int index) const;
[[nodiscard]] bool hasUnlockButton(int index) const;
[[nodiscard]] QRect unlockButtonRect(int index) const;
[[nodiscard]] bool hasButton(int index) const;
[[nodiscard]] QRect buttonRect(int index) const;
[[nodiscard]] QRect buttonRect(
const SectionInfo &info,
const RightButton &button) const;
[[nodiscard]] const RightButton &rightButton(int index) const;
[[nodiscard]] QRect emojiRect(int section, int index) const;
[[nodiscard]] int emojiRight() const;
[[nodiscard]] int emojiLeft() const;
[[nodiscard]] uint64 sectionSetId(int section) const;
[[nodiscard]] std::vector<StickerIcon> fillIcons();
int paintButtonGetWidth(
QPainter &p,
const SectionInfo &info,
bool selected,
QRect clip) const;
void paintEmptySearchResults(Painter &p);
void displaySet(uint64 setId);
void removeSet(uint64 setId);
void removeMegagroupSet(bool locally);
void initButton(RightButton &button, const QString &text, bool gradient);
[[nodiscard]] std::unique_ptr<Ui::RippleAnimation> createButtonRipple(
int section);
[[nodiscard]] QPoint buttonRippleTopLeft(int section) const;
[[nodiscard]] PowerSaving::Flag powerSavingFlag() const;
void repaintCustom(uint64 setId);
void fillRecent();
void fillRecentFrom(const std::vector<EmojiStatusId> &list);
[[nodiscard]] not_null<Ui::Text::CustomEmoji*> resolveCustomEmoji(
EmojiStatusId id,
not_null<DocumentData*> document,
uint64 setId);
[[nodiscard]] Ui::Text::CustomEmoji *resolveCustomRecent(
Core::RecentEmojiId customId);
[[nodiscard]] not_null<Ui::Text::CustomEmoji*> resolveCustomRecent(
DocumentId documentId);
[[nodiscard]] not_null<Ui::Text::CustomEmoji*> resolveCustomRecent(
EmojiStatusId id);
[[nodiscard]] Fn<void()> repaintCallback(
DocumentId documentId,
uint64 setId);
void showPreview();
void showPreviewFor(not_null<DocumentData*> document);
void ensureMediaPreview();
void applyNextSearchQuery();
const std::shared_ptr<Show> _show;
const ComposeFeatures _features;
const bool _onlyUnicodeEmoji;
Mode _mode = Mode::Full;
QWidget *_mediaPreviewParent = nullptr;
QMargins _mediaPreviewMargins;
std::unique_ptr<Ui::TabbedSearch> _search;
MTP::Sender _api;
const int _staticCount = 0;
StickersListFooter *_footer = nullptr;
std::unique_ptr<GradientPremiumStar> _premiumIcon;
std::unique_ptr<LocalStickersManager> _localSetsManager;
ChannelData *_megagroupSet = nullptr;
uint64 _megagroupSetIdRequested = 0;
Fn<std::unique_ptr<Ui::Text::CustomEmoji>(
DocumentId,
Fn<void()>)> _customRecentFactory;
int _counts[kEmojiSectionCount];
std::vector<RecentOne> _recent;
base::flat_set<DocumentId> _recentCustomIds;
base::flat_set<DocumentId> _freeEffects;
base::flat_set<uint64> _repaintsScheduled;
rpl::variable<int> _recentShownCount;
std::unique_ptr<Ui::Text::CustomEmojiPaintContext> _emojiPaintContext;
bool _recentPainted = false;
bool _grabbingChosen = false;
bool _paintAsPremium = false;
QVector<EmojiPtr> _emoji[kEmojiSectionCount];
std::vector<CustomSet> _custom;
base::flat_set<DocumentId> _restrictedCustomList;
base::flat_map<EmojiStatusId, CustomEmojiInstance> _customEmoji;
base::flat_map<
DocumentId,
std::unique_ptr<Ui::Text::CustomEmoji>> _customRecent;
Fn<QColor()> _customTextColor;
int _customSingleSize = 0;
bool _allowWithoutPremium = false;
Ui::RoundRect _overBg;
QImage _searchExpandCache;
std::unique_ptr<StickerPremiumMark> _premiumMark;
QImage _premiumMarkFrameCache;
mutable std::unique_ptr<Ui::RippleAnimation> _colorAllRipple;
bool _colorAllRippleForced = false;
rpl::lifetime _colorAllRippleForcedLifetime;
rpl::event_stream<std::vector<QString>> _searchQueries;
std::vector<QString> _nextSearchQuery;
std::vector<QString> _searchQuery;
base::flat_set<EmojiPtr> _searchEmoji;
base::flat_set<EmojiPtr> _searchEmojiPrevious;
base::flat_set<DocumentId> _searchCustomIds;
std::vector<RecentOne> _searchResults;
bool _searchMode = false;
int _rowsTop = 0;
int _rowsLeft = 0;
int _columnCount = 1;
QSize _singleSize;
QPoint _areaPosition;
QPoint _innerPosition;
QPoint _customPosition;
RightButton _add;
RightButton _unlock;
RightButton _restore;
Ui::RoundRect _collapsedBg;
OverState _selected;
OverState _pressed;
OverState _pickerSelected;
QPoint _lastMousePos;
object_ptr<EmojiColorPicker> _picker;
base::Timer _showPickerTimer;
base::Timer _previewTimer;
bool _previewShown = false;
base::unique_qptr<Window::MediaPreviewWidget> _mediaPreview;
rpl::event_stream<EmojiChosen> _chosen;
rpl::event_stream<FileChosen> _customChosen;
rpl::event_stream<> _jumpedToPremium;
};
tr::phrase<> EmojiCategoryTitle(int index);
} // namespace ChatHelpers

View File

@@ -0,0 +1,567 @@
/*
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/emoji_sets_manager.h"
#include "mtproto/dedicated_file_loader.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/effects/animations.h"
#include "ui/effects/radial_animation.h"
#include "ui/emoji_config.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "core/application.h"
#include "lang/lang_keys.h"
#include "main/main_account.h"
#include "storage/storage_cloud_blob.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include "styles/style_chat_helpers.h"
namespace Ui {
namespace Emoji {
namespace {
using namespace Storage::CloudBlob;
struct Set : public Blob {
QString previewPath;
};
inline auto PreviewPath(int i) {
return u":/gui/emoji/set%1_preview.webp"_q.arg(i);
}
const auto kSets = {
Set{ { 0, 0, 0, "Mac" }, PreviewPath(0) },
Set{ { 1, 2774, 8'455'034, "Android" }, PreviewPath(1) },
Set{ { 2, 2775, 5'713'503, "Twemoji" }, PreviewPath(2) },
Set{ { 3, 2776, 7'347'332, "JoyPixels" }, PreviewPath(3) },
};
using Loading = MTP::DedicatedLoader::Progress;
using SetState = BlobState;
class Loader final : public BlobLoader {
public:
Loader(
not_null<Main::Session*> session,
int id,
MTP::DedicatedLoader::Location location,
const QString &folder,
int size);
void destroy() override;
void unpack(const QString &path) override;
private:
void fail() override;
};
class Inner : public Ui::RpWidget {
public:
Inner(QWidget *parent, not_null<Main::Session*> session);
private:
void setupContent();
const not_null<Main::Session*> _session;
};
class Row : public Ui::RippleButton {
public:
Row(QWidget *widget, not_null<Main::Session*> session, const Set &set);
protected:
void paintEvent(QPaintEvent *e) override;
void onStateChanged(State was, StateChangeSource source) override;
private:
[[nodiscard]] bool showOver() const;
[[nodiscard]] bool showOver(State state) const;
void updateStatusColorOverride();
void setupContent(const Set &set);
void setupLabels(const Set &set);
void setupPreview(const Set &set);
void setupAnimation();
void paintPreview(QPainter &p) const;
void paintRadio(QPainter &p);
void setupHandler();
void load();
void radialAnimationCallback(crl::time now);
void updateLoadingToFinished();
const not_null<Main::Session*> _session;
int _id = 0;
bool _switching = false;
rpl::variable<SetState> _state;
Ui::FlatLabel *_status = nullptr;
std::array<QPixmap, 4> _preview;
Ui::Animations::Simple _toggled;
Ui::Animations::Simple _active;
std::unique_ptr<Ui::RadialAnimation> _loading;
};
base::unique_qptr<Loader> GlobalLoader;
rpl::event_stream<Loader*> GlobalLoaderValues;
void SetGlobalLoader(base::unique_qptr<Loader> loader) {
GlobalLoader = std::move(loader);
GlobalLoaderValues.fire(GlobalLoader.get());
}
int64 GetDownloadSize(int id) {
return ranges::find(kSets, id, &Set::id)->size;
}
[[nodiscard]] float64 CountProgress(not_null<const Loading*> loading) {
return (loading->size > 0)
? (loading->already / float64(loading->size))
: 0.;
}
MTP::DedicatedLoader::Location GetDownloadLocation(int id) {
const auto username = kCloudLocationUsername.utf16();
const auto i = ranges::find(kSets, id, &Set::id);
return MTP::DedicatedLoader::Location{ username, i->postId };
}
SetState ComputeState(int id) {
if (id == CurrentSetId()) {
return Active();
} else if (SetIsReady(id)) {
return Ready();
}
return Available{ GetDownloadSize(id) };
}
QString StateDescription(const SetState &state) {
return StateDescription(
state,
tr::lng_emoji_set_active);
}
bool GoodSetPartName(const QString &name) {
return (name == u"config.json"_q)
|| (name.startsWith(u"emoji_"_q) && name.endsWith(u".webp"_q));
}
bool UnpackSet(const QString &path, const QString &folder) {
return UnpackBlob(path, folder, GoodSetPartName);
}
Loader::Loader(
not_null<Main::Session*> session,
int id,
MTP::DedicatedLoader::Location location,
const QString &folder,
int size)
: BlobLoader(nullptr, session, id, location, folder, size) {
}
void Loader::unpack(const QString &path) {
const auto folder = internal::SetDataPath(id());
const auto weak = base::make_weak(this);
crl::async([=] {
if (UnpackSet(path, folder)) {
QFile(path).remove();
SwitchToSet(id(), crl::guard(weak, [=](bool success) {
if (success) {
destroy();
} else {
fail();
}
}));
} else {
crl::on_main(weak, [=] {
fail();
});
}
});
}
void Loader::destroy() {
Expects(GlobalLoader == this);
SetGlobalLoader(nullptr);
}
void Loader::fail() {
ClearNeedSwitchToId();
BlobLoader::fail();
}
Inner::Inner(QWidget *parent, not_null<Main::Session*> session)
: RpWidget(parent)
, _session(session) {
setupContent();
}
void Inner::setupContent() {
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
for (const auto &set : kSets) {
content->add(object_ptr<Row>(content, _session, set));
}
content->resizeToWidth(st::boxWidth);
Ui::ResizeFitChild(this, content);
}
Row::Row(QWidget *widget, not_null<Main::Session*> session, const Set &set)
: RippleButton(widget, st::defaultRippleAnimation)
, _session(session)
, _id(set.id)
, _state(Available{ set.size }) {
setupContent(set);
setupHandler();
}
void Row::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
const auto over = showOver();
const auto bg = over ? st::windowBgOver : st::windowBg;
p.fillRect(rect(), bg);
paintRipple(p, 0, 0);
paintPreview(p);
paintRadio(p);
}
void Row::paintPreview(QPainter &p) const {
const auto x = st::manageEmojiPreviewPadding.left();
const auto y = st::manageEmojiPreviewPadding.top();
const auto width = st::manageEmojiPreviewWidth;
const auto height = st::manageEmojiPreviewWidth;
auto &&preview = ranges::views::zip(_preview, ranges::views::ints(0, int(_preview.size())));
for (const auto &[pixmap, index] : preview) {
const auto row = (index / 2);
const auto column = (index % 2);
const auto left = x + (column ? width - st::manageEmojiPreview : 0);
const auto top = y + (row ? height - st::manageEmojiPreview : 0);
p.drawPixmap(left, top, pixmap);
}
}
void Row::paintRadio(QPainter &p) {
if (_loading && !_loading->animating()) {
_loading = nullptr;
}
const auto loading = _loading
? _loading->computeState()
: Ui::RadialState{ 0., 0, arc::kFullLength };
const auto isToggledSet = v::is<Active>(_state.current());
const auto isActiveSet = isToggledSet || v::is<Loading>(_state.current());
const auto toggled = _toggled.value(isToggledSet ? 1. : 0.);
const auto active = _active.value(isActiveSet ? 1. : 0.);
const auto _st = &st::defaultRadio;
PainterHighQualityEnabler hq(p);
const auto left = width()
- st::manageEmojiMarginRight
- _st->diameter
- _st->thickness;
const auto top = (height() - _st->diameter - _st->thickness) / 2;
const auto outerWidth = width();
auto pen = anim::pen(_st->untoggledFg, _st->toggledFg, active);
pen.setWidth(_st->thickness);
pen.setCapStyle(Qt::RoundCap);
p.setPen(pen);
p.setBrush(_st->bg);
const auto rect = style::rtlrect(QRectF(
left,
top,
_st->diameter,
_st->diameter
).marginsRemoved(QMarginsF(
_st->thickness / 2.,
_st->thickness / 2.,
_st->thickness / 2.,
_st->thickness / 2.
)), outerWidth);
if (loading.shown > 0 && anim::Disabled()) {
anim::DrawStaticLoading(
p,
rect,
_st->thickness,
pen.color(),
_st->bg);
} else if (loading.arcLength < arc::kFullLength) {
p.drawArc(rect, loading.arcFrom, loading.arcLength);
} else {
p.drawEllipse(rect);
}
if (toggled > 0 && (!_loading || !anim::Disabled())) {
p.setPen(Qt::NoPen);
p.setBrush(anim::brush(_st->untoggledFg, _st->toggledFg, toggled));
const auto skip0 = _st->diameter / 2.;
const auto skip1 = _st->skip / 10.;
const auto checkSkip = skip0 * (1. - toggled) + skip1 * toggled;
p.drawEllipse(style::rtlrect(QRectF(
left,
top,
_st->diameter,
_st->diameter
).marginsRemoved(QMarginsF(
checkSkip,
checkSkip,
checkSkip,
checkSkip
)), outerWidth));
}
}
bool Row::showOver(State state) const {
return (!(state & StateFlag::Disabled))
&& (state & (StateFlag::Over | StateFlag::Down));
}
bool Row::showOver() const {
return showOver(state());
}
void Row::onStateChanged(State was, StateChangeSource source) {
RippleButton::onStateChanged(was, source);
if (showOver() != showOver(was)) {
updateStatusColorOverride();
}
}
void Row::updateStatusColorOverride() {
const auto isToggledSet = v::is<Active>(_state.current());
const auto toggled = _toggled.value(isToggledSet ? 1. : 0.);
const auto over = showOver();
if (toggled == 0. && !over) {
_status->setTextColorOverride(std::nullopt);
} else {
_status->setTextColorOverride(anim::color(
over ? st::contactsStatusFgOver : st::contactsStatusFg,
st::contactsStatusFgOnline,
toggled));
}
}
void Row::setupContent(const Set &set) {
_state = GlobalLoaderValues.events_starting_with(
GlobalLoader.get()
) | rpl::map([=](Loader *loader) {
return (loader && loader->id() == _id)
? loader->state()
: rpl::single(rpl::empty) | rpl::then(
Updated()
) | rpl::map([=] {
return ComputeState(_id);
});
}) | rpl::flatten_latest(
) | rpl::filter([=](const SetState &state) {
return !v::is<Failed>(_state.current())
|| !v::is<Available>(state);
});
setupLabels(set);
setupPreview(set);
setupAnimation();
const auto height = st::manageEmojiPreviewPadding.top()
+ st::manageEmojiPreviewHeight
+ st::manageEmojiPreviewPadding.bottom();
resize(width(), height);
}
void Row::setupHandler() {
clicks(
) | rpl::filter([=] {
const auto &state = _state.current();
return !_switching && (v::is<Ready>(state)
|| v::is<Available>(state));
}) | rpl::on_next([=] {
if (v::is<Available>(_state.current())) {
load();
return;
}
_switching = true;
SwitchToSet(_id, crl::guard(this, [=](bool success) {
_switching = false;
if (!success) {
load();
} else if (GlobalLoader && GlobalLoader->id() == _id) {
GlobalLoader->destroy();
}
}));
}, lifetime());
_state.value(
) | rpl::map([=](const SetState &state) {
return v::is<Ready>(state) || v::is<Available>(state);
}) | rpl::on_next([=](bool active) {
setDisabled(!active);
setPointerCursor(active);
}, lifetime());
}
void Row::load() {
LoadAndSwitchTo(_session, _id);
}
void Row::setupLabels(const Set &set) {
using namespace rpl::mappers;
const auto name = Ui::CreateChild<Ui::FlatLabel>(
this,
set.name,
st::localStorageRowTitle);
name->setAttribute(Qt::WA_TransparentForMouseEvents);
_status = Ui::CreateChild<Ui::FlatLabel>(
this,
_state.value() | rpl::map(StateDescription),
st::localStorageRowSize);
_status->setAttribute(Qt::WA_TransparentForMouseEvents);
sizeValue(
) | rpl::on_next([=](QSize size) {
const auto left = st::manageEmojiPreviewPadding.left()
+ st::manageEmojiPreviewWidth
+ st::manageEmojiPreviewPadding.right();
const auto namey = st::manageEmojiPreviewPadding.top()
+ st::manageEmojiNameTop;
const auto statusy = st::manageEmojiPreviewPadding.top()
+ st::manageEmojiStatusTop;
name->moveToLeft(left, namey);
_status->moveToLeft(left, statusy);
}, name->lifetime());
}
void Row::setupPreview(const Set &set) {
const auto size = st::manageEmojiPreview * style::DevicePixelRatio();
const auto original = QImage(set.previewPath);
const auto full = original.height();
auto &&preview = ranges::views::zip(_preview, ranges::views::ints(0, int(_preview.size())));
for (auto &&[pixmap, index] : preview) {
pixmap = Ui::PixmapFromImage(original.copy(
{ full * index, 0, full, full }
).scaledToWidth(size, Qt::SmoothTransformation));
pixmap.setDevicePixelRatio(style::DevicePixelRatio());
}
}
void Row::updateLoadingToFinished() {
_loading->update(
v::is<Failed>(_state.current()) ? 0. : 1.,
true,
crl::now());
}
void Row::radialAnimationCallback(crl::time now) {
const auto updated = [&] {
const auto state = _state.current();
if (const auto loading = std::get_if<Loading>(&state)) {
return _loading->update(CountProgress(loading), false, now);
} else {
updateLoadingToFinished();
}
return false;
}();
if (!anim::Disabled() || updated) {
update();
}
}
void Row::setupAnimation() {
using namespace rpl::mappers;
_state.value(
) | rpl::on_next([=](const SetState &state) {
update();
}, lifetime());
_state.value(
) | rpl::map(
_1 == SetState{ Active() }
) | rpl::distinct_until_changed(
) | rpl::on_next([=](bool toggled) {
_toggled.start(
[=] { updateStatusColorOverride(); update(); },
toggled ? 0. : 1.,
toggled ? 1. : 0.,
st::defaultRadio.duration);
}, lifetime());
_state.value(
) | rpl::map([](const SetState &state) {
return v::is<Loading>(state) || v::is<Active>(state);
}) | rpl::distinct_until_changed(
) | rpl::on_next([=](bool active) {
_active.start(
[=] { update(); },
active ? 0. : 1.,
active ? 1. : 0.,
st::defaultRadio.duration);
}, lifetime());
_state.value(
) | rpl::map([](const SetState &state) {
return std::get_if<Loading>(&state);
}) | rpl::distinct_until_changed(
) | rpl::on_next([=](const Loading *loading) {
if (loading && !_loading) {
_loading = std::make_unique<Ui::RadialAnimation>(
[=](crl::time now) { radialAnimationCallback(now); });
_loading->start(CountProgress(loading));
} else if (!loading && _loading) {
updateLoadingToFinished();
}
}, lifetime());
_toggled.stop();
_active.stop();
updateStatusColorOverride();
}
} // namespace
ManageSetsBox::ManageSetsBox(QWidget*, not_null<Main::Session*> session)
: _session(session) {
}
void ManageSetsBox::prepare() {
const auto inner = setInnerWidget(object_ptr<Inner>(this, _session));
setTitle(tr::lng_emoji_manage_sets());
addButton(tr::lng_close(), [=] { closeBox(); });
setDimensionsToContent(st::boxWidth, inner);
}
void LoadAndSwitchTo(not_null<Main::Session*> session, int id) {
if (!ranges::contains(kSets, id, &Set::id)) {
ClearNeedSwitchToId();
return;
}
SetGlobalLoader(base::make_unique_q<Loader>(
session,
id,
GetDownloadLocation(id),
internal::SetDataPath(id),
GetDownloadSize(id)));
}
} // namespace Emoji
} // namespace Ui

View File

@@ -0,0 +1,33 @@
/*
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/layers/box_content.h"
namespace Main {
class Session;
} // namespace Main
namespace Ui {
namespace Emoji {
class ManageSetsBox final : public Ui::BoxContent {
public:
ManageSetsBox(QWidget*, not_null<Main::Session*> session);
private:
void prepare() override;
const not_null<Main::Session*> _session;
};
void LoadAndSwitchTo(not_null<Main::Session*> session, int id);
} // namespace Emoji
} // namespace Ui

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 "ui/effects/animations.h"
#include "ui/rp_widget.h"
#include "base/unique_qptr.h"
#include "base/timer.h"
#include <QtWidgets/QTextEdit>
namespace style {
struct EmojiSuggestions;
} // namespace style
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class InnerDropdown;
class InputField;
} // namespace Ui
namespace Ui::Text {
class CustomEmoji;
} // namespace Ui::Text
namespace Ui::Emoji {
class SuggestionsWidget;
using SuggestionsQuery = std::variant<QString, EmojiPtr>;
class SuggestionsController final : public QObject {
public:
struct Options {
bool suggestExactFirstWord = true;
bool suggestCustomEmoji = false;
Fn<bool(not_null<DocumentData*>)> allowCustomWithoutPremium;
const style::EmojiSuggestions *st = nullptr;
};
SuggestionsController(
not_null<QWidget*> parent,
not_null<QWidget*> outer,
not_null<QTextEdit*> field,
not_null<Main::Session*> session,
const Options &options);
void raise();
void setReplaceCallback(Fn<void(
int from,
int till,
const QString &replacement,
const QString &customEmojiData)> callback);
static not_null<SuggestionsController*> Init(
not_null<QWidget*> outer,
not_null<Ui::InputField*> field,
not_null<Main::Session*> session) {
return Init(outer, field, session, {});
}
static not_null<SuggestionsController*> Init(
not_null<QWidget*> outer,
not_null<Ui::InputField*> field,
not_null<Main::Session*> session,
const Options &options);
private:
void handleCursorPositionChange();
void handleTextChange();
void showWithQuery(SuggestionsQuery query);
[[nodiscard]] SuggestionsQuery getEmojiQuery();
void suggestionsUpdated(bool visible);
void updateGeometry();
void updateForceHidden();
void replaceCurrent(
const QString &replacement,
const QString &customEmojiData);
bool fieldFilter(not_null<QEvent*> event);
bool outerFilter(not_null<QEvent*> event);
const style::EmojiSuggestions &_st;
bool _shown = false;
bool _forceHidden = false;
int _queryStartPosition = 0;
int _emojiQueryLength = 0;
bool _ignoreCursorPositionChange = false;
bool _textChangeAfterKeyPress = false;
QPointer<QTextEdit> _field;
const not_null<Main::Session*> _session;
Fn<void(
int from,
int till,
const QString &replacement,
const QString &customEmojiData)> _replaceCallback;
base::unique_qptr<InnerDropdown> _container;
QPointer<SuggestionsWidget> _suggestions;
base::unique_qptr<QObject> _fieldFilter;
base::unique_qptr<QObject> _outerFilter;
base::Timer _showExactTimer;
bool _keywordsRefreshed = false;
SuggestionsQuery _lastShownQuery;
Options _options;
rpl::lifetime _lifetime;
};
} // namespace Ui::Emoji

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,227 @@
/*
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 "api/api_common.h"
#include "ui/effects/animations.h"
#include "ui/effects/message_sending_animation_common.h"
#include "ui/rp_widget.h"
#include "base/timer.h"
#include "base/object_ptr.h"
namespace style {
struct EmojiPan;
} // namespace style
namespace Ui {
class PopupMenu;
class ScrollArea;
class InputField;
} // namespace Ui
namespace Lottie {
class SinglePlayer;
class FrameRenderer;
} // namespace Lottie;
namespace Main {
class Session;
} // namespace Main
namespace Window {
class SessionController;
} // namespace Window
namespace Data {
class DocumentMedia;
} // namespace Data
namespace SendMenu {
struct Details;
} // namespace SendMenu
namespace ChatHelpers {
struct ComposeFeatures;
struct FileChosen;
class Show;
enum class FieldAutocompleteChooseMethod {
ByEnter,
ByTab,
ByClick,
};
class FieldAutocomplete final : public Ui::RpWidget {
public:
FieldAutocomplete(
QWidget *parent,
std::shared_ptr<Show> show,
const style::EmojiPan *stOverride = nullptr);
~FieldAutocomplete();
[[nodiscard]] std::shared_ptr<Show> uiShow() const;
bool clearFilteredBotCommands();
void showFiltered(
not_null<PeerData*> peer,
QString query,
bool addInlineBots);
void showStickers(EmojiPtr emoji);
[[nodiscard]] EmojiPtr stickersEmoji() const;
void setBoundings(QRect boundings);
[[nodiscard]] const QString &filter() const;
[[nodiscard]] ChatData *chat() const;
[[nodiscard]] ChannelData *channel() const;
[[nodiscard]] UserData *user() const;
[[nodiscard]] int32 innerTop();
[[nodiscard]] int32 innerBottom();
bool eventFilter(QObject *obj, QEvent *e) override;
using ChooseMethod = FieldAutocompleteChooseMethod;
struct MentionChosen {
not_null<UserData*> user;
QString mention;
ChooseMethod method = ChooseMethod::ByEnter;
};
struct HashtagChosen {
QString hashtag;
ChooseMethod method = ChooseMethod::ByEnter;
};
struct BotCommandChosen {
not_null<UserData*> user;
QString command;
ChooseMethod method = ChooseMethod::ByEnter;
};
using StickerChosen = FileChosen;
enum class Type {
Mentions,
Hashtags,
BotCommands,
Stickers,
};
bool chooseSelected(ChooseMethod method) const;
[[nodiscard]] bool stickersShown() const {
return !_srows.empty();
}
[[nodiscard]] bool overlaps(const QRect &globalRect) {
if (isHidden() || !testAttribute(Qt::WA_OpaquePaintEvent)) {
return false;
}
return rect().contains(QRect(mapFromGlobal(globalRect.topLeft()), globalRect.size()));
}
void setModerateKeyActivateCallback(Fn<bool(int)> callback) {
_moderateKeyActivateCallback = std::move(callback);
}
void setSendMenuDetails(Fn<SendMenu::Details()> &&callback);
void hideFast();
void showAnimated();
void hideAnimated();
void requestRefresh();
[[nodiscard]] rpl::producer<> refreshRequests() const;
void requestStickersUpdate();
[[nodiscard]] rpl::producer<> stickersUpdateRequests() const;
[[nodiscard]] rpl::producer<MentionChosen> mentionChosen() const;
[[nodiscard]] rpl::producer<HashtagChosen> hashtagChosen() const;
[[nodiscard]] rpl::producer<BotCommandChosen> botCommandChosen() const;
[[nodiscard]] rpl::producer<StickerChosen> stickerChosen() const;
[[nodiscard]] rpl::producer<Type> choosingProcesses() const;
protected:
void paintEvent(QPaintEvent *e) override;
private:
class Inner;
friend class Inner;
struct StickerSuggestion;
struct MentionRow;
struct BotCommandRow;
using HashtagRows = std::vector<QString>;
using BotCommandRows = std::vector<BotCommandRow>;
using StickerRows = std::vector<StickerSuggestion>;
using MentionRows = std::vector<MentionRow>;
void animationCallback();
void hideFinish();
void updateFiltered(bool resetScroll = false);
void recount(bool resetScroll = false);
StickerRows getStickerSuggestions();
const std::shared_ptr<Show> _show;
const not_null<Main::Session*> _session;
const style::EmojiPan &_st;
QPixmap _cache;
MentionRows _mrows;
HashtagRows _hrows;
BotCommandRows _brows;
StickerRows _srows;
void rowsUpdated(
MentionRows &&mrows,
HashtagRows &&hrows,
BotCommandRows &&brows,
StickerRows &&srows,
bool resetScroll);
object_ptr<Ui::ScrollArea> _scroll;
QPointer<Inner> _inner;
ChatData *_chat = nullptr;
UserData *_user = nullptr;
ChannelData *_channel = nullptr;
EmojiPtr _emoji;
uint64 _stickersSeed = 0;
Type _type = Type::Mentions;
QString _filter;
QRect _boundings;
bool _addInlineBots;
bool _hiding = false;
Ui::Animations::Simple _a_opacity;
rpl::event_stream<> _refreshRequests;
rpl::event_stream<> _stickersUpdateRequests;
Fn<bool(int)> _moderateKeyActivateCallback;
};
struct FieldAutocompleteDescriptor {
not_null<QWidget*> parent;
std::shared_ptr<Show> show;
not_null<Ui::InputField*> field;
const style::EmojiPan *stOverride = nullptr;
not_null<PeerData*> peer;
Fn<ComposeFeatures()> features;
Fn<SendMenu::Details()> sendMenuDetails;
Fn<void()> stickerChoosing;
Fn<void(FileChosen&&)> stickerChosen;
Fn<void(TextWithTags)> setText;
Fn<void(QString)> sendBotCommand;
Fn<void(QString)> processShortcut;
Fn<bool(int)> moderateKeyActivateCallback;
};
void InitFieldAutocomplete(
std::unique_ptr<FieldAutocomplete> &autocomplete,
FieldAutocompleteDescriptor &&descriptor);
} // namespace ChatHelpers

View File

@@ -0,0 +1,36 @@
/*
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_characters_count_manager.h"
FieldCharsCountManager::FieldCharsCountManager() = default;
void FieldCharsCountManager::setCount(int count) {
_previous = _current;
_current = count;
if (_previous != _current) {
constexpr auto kMax = 15;
const auto was = (_previous > kMax);
const auto now = (_current > kMax);
if (was != now) {
_isLimitExceeded = now;
_limitExceeds.fire({});
}
}
}
int FieldCharsCountManager::count() const {
return _current;
}
bool FieldCharsCountManager::isLimitExceeded() const {
return _isLimitExceeded;
}
rpl::producer<> FieldCharsCountManager::limitExceeds() const {
return _limitExceeds.events();
}

View File

@@ -0,0 +1,26 @@
/*
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 FieldCharsCountManager final {
public:
FieldCharsCountManager();
void setCount(int count);
[[nodiscard]] int count() const;
[[nodiscard]] bool isLimitExceeded() const;
[[nodiscard]] rpl::producer<> limitExceeds() const;
private:
int _current = 0;
int _previous = 0;
bool _isLimitExceeded = false;
rpl::event_stream<> _limitExceeds;
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,227 @@
/*
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 "chat_helpers/tabbed_selector.h"
#include "base/timer.h"
#include "inline_bots/inline_bot_layout_item.h"
#include "layout/layout_mosaic.h"
#include <QtCore/QTimer>
namespace style {
struct ComposeIcons;
} // namespace style
namespace Api {
struct SendOptions;
} // namespace Api
namespace InlineBots {
namespace Layout {
class ItemBase;
} // namespace Layout
class Result;
} // namespace InlineBots
namespace Ui {
class PopupMenu;
class RoundButton;
class TabbedSearch;
} // namespace Ui
namespace Window {
class SessionController;
} // namespace Window
namespace SendMenu {
struct Details;
} // namespace SendMenu
namespace Data {
class StickersSet;
} // namespace Data
namespace ChatHelpers {
void AddGifAction(
Fn<void(QString, Fn<void()> &&, const style::icon*)> callback,
std::shared_ptr<Show> show,
not_null<DocumentData*> document,
const style::ComposeIcons *iconsOverride = nullptr);
class StickersListFooter;
struct StickerIcon;
struct GifSection;
struct GifsListDescriptor {
std::shared_ptr<Show> show;
Fn<bool()> paused;
const style::EmojiPan *st = nullptr;
};
class GifsListWidget final
: public TabbedSelector::Inner
, public InlineBots::Layout::Context {
public:
GifsListWidget(
QWidget *parent,
not_null<Window::SessionController*> controller,
PauseReason level);
GifsListWidget(QWidget *parent, GifsListDescriptor &&descriptor);
rpl::producer<FileChosen> fileChosen() const;
rpl::producer<PhotoChosen> photoChosen() const;
rpl::producer<InlineChosen> inlineResultChosen() const;
void refreshRecent() override;
void preloadImages() override;
void clearSelection() override;
object_ptr<TabbedSelector::InnerFooter> createFooter() override;
void inlineItemLayoutChanged(const InlineBots::Layout::ItemBase *layout) override;
void inlineItemRepaint(const InlineBots::Layout::ItemBase *layout) override;
bool inlineItemVisible(const InlineBots::Layout::ItemBase *layout) override;
Data::FileOrigin inlineItemFileOrigin() override;
void afterShown() override;
void beforeHiding() override;
void setInlineQueryPeer(PeerData *peer) {
_inlineQueryPeer = peer;
}
void searchForGifs(const QString &query);
void sendInlineRequest();
void cancelled();
rpl::producer<> cancelRequests() const;
base::unique_qptr<Ui::PopupMenu> fillContextMenu(
const SendMenu::Details &details) override;
~GifsListWidget();
protected:
void visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void paintEvent(QPaintEvent *e) override;
void leaveEventHook(QEvent *e) override;
void leaveToChildEvent(QEvent *e, QWidget *child) override;
void enterFromChildEvent(QEvent *e, QWidget *child) override;
TabbedSelector::InnerFooter *getFooter() const override;
void processHideFinished() override;
void processPanelHideFinished() override;
int countDesiredHeight(int newWidth) override;
private:
enum class Section {
Inlines,
Gifs,
};
using InlineResult = InlineBots::Result;
using InlineResults = std::vector<std::shared_ptr<InlineResult>>;
using LayoutItem = InlineBots::Layout::ItemBase;
struct InlineCacheEntry {
QString nextOffset;
InlineResults results;
};
void setupSearch();
void clearHeavyData();
void cancelGifsSearch();
void switchToSavedGifs();
void refreshSavedGifs();
int refreshInlineRows(const InlineCacheEntry *results, bool resultsDeleted);
void checkLoadMore();
int32 showInlineRows(bool newResults);
bool refreshInlineRows(int32 *added = 0);
void inlineResultsDone(const MTPmessages_BotResults &result);
void updateSelected();
void paintInlineItems(Painter &p, QRect clip);
void refreshIcons();
[[nodiscard]] std::vector<StickerIcon> fillIcons();
void updateInlineItems();
void repaintItems(crl::time now = 0);
void showPreview();
void clearInlineRows(bool resultsDeleted);
LayoutItem *layoutPrepareSavedGif(not_null<DocumentData*> document);
LayoutItem *layoutPrepareInlineResult(
std::shared_ptr<InlineResult> result);
void deleteUnusedGifLayouts();
void deleteUnusedInlineLayouts();
int validateExistingInlineRows(const InlineResults &results);
void selectInlineResult(
int index,
Api::SendOptions options,
bool forceSend = false,
TextWithTags caption = {});
const std::shared_ptr<Show> _show;
std::unique_ptr<Ui::TabbedSearch> _search;
MTP::Sender _api;
Section _section = Section::Gifs;
crl::time _lastScrolledAt = 0;
crl::time _lastUpdatedAt = 0;
base::Timer _updateInlineItems;
bool _inlineWithThumb = false;
std::map<
not_null<DocumentData*>,
std::unique_ptr<LayoutItem>> _gifLayouts;
std::map<
not_null<InlineResult*>,
std::unique_ptr<LayoutItem>> _inlineLayouts;
StickersListFooter *_footer = nullptr;
std::vector<GifSection> _sections;
base::flat_map<uint64, std::unique_ptr<Data::StickersSet>> _fakeSets;
uint64 _chosenSetId = 0;
Mosaic::Layout::MosaicLayout<LayoutItem> _mosaic;
int _selected = -1;
int _pressed = -1;
QPoint _lastMousePos;
base::Timer _previewTimer;
bool _previewShown = false;
std::map<QString, std::unique_ptr<InlineCacheEntry>> _inlineCache;
QTimer _inlineRequestTimer;
UserData *_searchBot = nullptr;
mtpRequestId _searchBotRequestId = 0;
PeerData *_inlineQueryPeer = nullptr;
QString _inlineQuery, _inlineNextQuery, _inlineNextOffset;
mtpRequestId _inlineRequestId = 0;
rpl::event_stream<FileChosen> _fileChosen;
rpl::event_stream<PhotoChosen> _photoChosen;
rpl::event_stream<InlineChosen> _inlineResultChosen;
rpl::event_stream<> _cancelled;
};
} // namespace ChatHelpers

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,218 @@
/*
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 "chat_helpers/compose/compose_features.h"
#include "ui/widgets/fields/input_field.h"
#ifndef TDESKTOP_DISABLE_SPELLCHECK
#include "boxes/dictionaries_manager.h"
#include "spellcheck/spelling_highlighter.h"
#endif // TDESKTOP_DISABLE_SPELLCHECK
#include <QtGui/QClipboard>
namespace tr {
struct now_t;
} // namespace tr
namespace Main {
class Session;
class SessionShow;
} // namespace Main
namespace Window {
class SessionController;
} // namespace Window
namespace ChatHelpers {
enum class PauseReason;
class Show;
} // namespace ChatHelpers
namespace HistoryView::Controls {
struct WriteRestriction;
} // namespace HistoryView::Controls
namespace Ui {
class GenericBox;
class PopupMenu;
class Show;
} // namespace Ui
[[nodiscard]] QString PrepareMentionTag(not_null<UserData*> user);
[[nodiscard]] TextWithTags PrepareEditText(not_null<HistoryItem*> item);
[[nodiscard]] bool EditTextChanged(
not_null<HistoryItem*> item,
TextWithTags updated);
Fn<bool(
Ui::InputField::EditLinkSelection selection,
TextWithTags text,
QString link,
Ui::InputField::EditLinkAction action)> DefaultEditLinkCallback(
std::shared_ptr<Main::SessionShow> show,
not_null<Ui::InputField*> field,
const style::InputField *fieldStyle = nullptr);
Fn<void(QString now, Fn<void(QString)> save)> DefaultEditLanguageCallback(
std::shared_ptr<Ui::Show> show);
struct MessageFieldHandlersArgs {
not_null<Main::Session*> session;
std::shared_ptr<Main::SessionShow> show; // may be null
not_null<Ui::InputField*> field;
Fn<bool()> customEmojiPaused;
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji;
const style::InputField *fieldStyle = nullptr;
base::flat_set<QString> allowMarkdownTags;
};
void InitMessageFieldHandlers(MessageFieldHandlersArgs &&args);
void InitMessageFieldHandlers(
not_null<Window::SessionController*> controller,
not_null<Ui::InputField*> field,
ChatHelpers::PauseReason pauseReasonLevel,
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji = nullptr);
void InitMessageField(
std::shared_ptr<ChatHelpers::Show> show,
not_null<Ui::InputField*> field,
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji);
void InitMessageField(
not_null<Window::SessionController*> controller,
not_null<Ui::InputField*> field,
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji);
void InitSpellchecker(
std::shared_ptr<Main::SessionShow> show,
not_null<Ui::InputField*> field,
bool skipDictionariesManager = false);
[[nodiscard]] Fn<void(not_null<Ui::InputField*>)> FactcheckFieldIniter(
std::shared_ptr<Main::SessionShow> show);
bool HasSendText(not_null<const Ui::InputField*> field);
void InitMessageFieldFade(
not_null<Ui::InputField*> field,
const style::color &bg);
struct InlineBotQuery {
QString query;
QString username;
UserData *bot = nullptr;
bool lookingUpBot = false;
};
InlineBotQuery ParseInlineBotQuery(
not_null<Main::Session*> session,
not_null<const Ui::InputField*> field);
struct AutocompleteQuery {
QString query;
bool fromStart = false;
};
AutocompleteQuery ParseMentionHashtagBotCommandQuery(
not_null<const Ui::InputField*> field,
ChatHelpers::ComposeFeatures features);
struct MessageLinkRange {
int start = 0;
int length = 0;
QString custom;
friend inline auto operator<=>(
const MessageLinkRange&,
const MessageLinkRange&) = default;
friend inline bool operator==(
const MessageLinkRange&,
const MessageLinkRange&) = default;
};
class MessageLinksParser final : private QObject {
public:
MessageLinksParser(not_null<Ui::InputField*> field);
void parseNow();
void setDisabled(bool disabled);
[[nodiscard]] const rpl::variable<QStringList> &list() const {
return _list;
}
[[nodiscard]] const std::vector<MessageLinkRange> &ranges() const {
return _ranges;
}
private:
bool eventFilter(QObject *object, QEvent *event) override;
void parse();
void applyRanges(const QString &text);
not_null<Ui::InputField*> _field;
rpl::variable<QStringList> _list;
std::vector<MessageLinkRange> _ranges;
int _lastLength = 0;
bool _disabled = false;
base::Timer _timer;
rpl::lifetime _lifetime;
};
[[nodiscard]] base::unique_qptr<Ui::RpWidget> CreateDisabledFieldView(
QWidget *parent,
not_null<PeerData*> peer);
[[nodiscard]] std::unique_ptr<Ui::RpWidget> TextErrorSendRestriction(
QWidget *parent,
const QString &text);
[[nodiscard]] std::unique_ptr<Ui::RpWidget> PremiumRequiredSendRestriction(
QWidget *parent,
not_null<UserData*> user,
not_null<Window::SessionController*> controller);
[[nodiscard]] auto BoostsToLiftWriteRestriction(
not_null<QWidget*> parent,
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer,
int boosts)
-> std::unique_ptr<Ui::AbstractButton>;
struct FreezeInfoStyleOverride {
const style::Box *box = nullptr;
const style::FlatLabel *title = nullptr;
const style::FlatLabel *subtitle = nullptr;
const style::icon *violationIcon = nullptr;
const style::icon *readOnlyIcon = nullptr;
const style::icon *appealIcon = nullptr;
const style::FlatLabel *infoTitle = nullptr;
const style::FlatLabel *infoAbout = nullptr;
};
[[nodiscard]] FreezeInfoStyleOverride DarkFreezeInfoStyle();
enum class FrozenWriteRestrictionType {
MessageField,
DialogsList,
};
[[nodiscard]] std::unique_ptr<Ui::AbstractButton> FrozenWriteRestriction(
not_null<QWidget*> parent,
std::shared_ptr<ChatHelpers::Show> show,
FrozenWriteRestrictionType type,
FreezeInfoStyleOverride st = {});
void SelectTextInFieldWithMargins(
not_null<Ui::InputField*> field,
const TextSelection &selection);
[[nodiscard]] TextWithEntities PaidSendButtonText(tr::now_t, int stars);
[[nodiscard]] rpl::producer<TextWithEntities> PaidSendButtonText(
rpl::producer<int> stars,
rpl::producer<QString> fallback = nullptr);
void FrozenInfoBox(
not_null<Ui::GenericBox*> box,
not_null<Main::Session*> session,
FreezeInfoStyleOverride st);

View File

@@ -0,0 +1,57 @@
/*
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/share_message_phrase_factory.h"
#include "data/data_peer.h"
#include "lang/lang_keys.h"
#include "ui/text/text_utilities.h"
namespace ChatHelpers {
rpl::producer<TextWithEntities> ForwardedMessagePhrase(
const ForwardedMessagePhraseArgs &args) {
if (args.toCount <= 1) {
Assert(args.to1);
if (args.to1->isSelf()) {
if (args.toSelfWithPremiumIsEmpty && args.to1->isPremium()) {
return {};
}
return (args.singleMessage
? tr::lng_share_message_to_saved_messages
: tr::lng_share_messages_to_saved_messages)(
tr::rich);
} else {
return (args.singleMessage
? tr::lng_share_message_to_chat
: tr::lng_share_messages_to_chat)(
lt_chat,
rpl::single(TextWithEntities{ args.to1->name() }),
tr::rich);
}
} else if ((args.toCount == 2) && (args.to1 && args.to2)) {
return (args.singleMessage
? tr::lng_share_message_to_two_chats
: tr::lng_share_messages_to_two_chats)(
lt_user,
rpl::single(TextWithEntities{ args.to1->name() }),
lt_chat,
rpl::single(TextWithEntities{ args.to2->name() }),
tr::rich);
} else {
return (args.singleMessage
? tr::lng_share_message_to_many_chats
: tr::lng_share_messages_to_many_chats)(
lt_count,
rpl::single(args.toCount) | tr::to_count(),
tr::rich);
}
}
} // namespace ChatHelpers

View File

@@ -0,0 +1,25 @@
/*
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 PeerData;
namespace ChatHelpers {
struct ForwardedMessagePhraseArgs final {
size_t toCount = 0;
bool singleMessage = false;
PeerData *to1 = nullptr;
PeerData *to2 = nullptr;
bool toSelfWithPremiumIsEmpty = true;
};
rpl::producer<TextWithEntities> ForwardedMessagePhrase(
const ForwardedMessagePhraseArgs &args);
} // namespace ChatHelpers

View File

@@ -0,0 +1,481 @@
/*
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/spellchecker_common.h"
#ifndef TDESKTOP_DISABLE_SPELLCHECK
#include "base/platform/base_platform_info.h"
#include "base/zlib_help.h"
#include "data/data_session.h"
#include "lang/lang_instance.h"
#include "lang/lang_keys.h"
#include "main/main_account.h"
#include "main/main_domain.h"
#include "main/main_session.h"
#include "mainwidget.h"
#include "spellcheck/platform/platform_spellcheck.h"
#include "spellcheck/spellcheck_utils.h"
#include "spellcheck/spellcheck_value.h"
#include "core/application.h"
#include "core/core_settings.h"
#include <QtGui/QGuiApplication>
#include <QtGui/QInputMethod>
namespace Spellchecker {
namespace {
using namespace Storage::CloudBlob;
constexpr auto kDictExtensions = { "dic", "aff" };
constexpr auto kExceptions = {
AppFile,
"\xd0\xa2\xd0\xb5\xd0\xbb\xd0\xb5\xd0\xb3\xd1\x80\xd0\xb0\xd0\xbc"_cs,
};
constexpr auto kLangsForLWC = { QLocale::English, QLocale::Portuguese };
constexpr auto kDefaultCountries = { QLocale::UnitedStates, QLocale::Brazil };
// Language With Country.
inline auto LWC(QLocale::Language language, QLocale::Country country) {
if (ranges::contains(kDefaultCountries, country)) {
return int(language);
}
return (language * 1000) + country;
}
inline auto LanguageFromLocale(QLocale loc) {
const auto locLang = loc.language();
return (ranges::contains(kLangsForLWC, locLang)
&& (loc.country() != QLocale::AnyCountry))
? LWC(locLang, loc.country())
: int(locLang);
}
const auto kDictionaries = {
Dict{{ QLocale::English, 649, 174'516, "English" }}, // en_US
Dict{{ QLocale::Bulgarian, 594, 229'658, "\xd0\x91\xd1\x8a\xd0\xbb\xd0\xb3\xd0\xb0\xd1\x80\xd1\x81\xd0\xba\xd0\xb8" }}, // bg_BG
Dict{{ QLocale::Catalan, 595, 417'611, "\x43\x61\x74\x61\x6c\xc3\xa0" }}, // ca_ES
Dict{{ QLocale::Czech, 596, 860'286, "\xc4\x8c\x65\xc5\xa1\x74\x69\x6e\x61" }}, // cs_CZ
Dict{{ QLocale::Welsh, 597, 177'305, "\x43\x79\x6d\x72\x61\x65\x67" }}, // cy_GB
Dict{{ QLocale::Danish, 598, 345'874, "\x44\x61\x6e\x73\x6b" }}, // da_DK
Dict{{ QLocale::German, 599, 2'412'780, "\x44\x65\x75\x74\x73\x63\x68" }}, // de_DE
Dict{{ QLocale::Greek, 600, 1'389'160, "\xce\x95\xce\xbb\xce\xbb\xce\xb7\xce\xbd\xce\xb9\xce\xba\xce\xac" }}, // el_GR
Dict{{ LWC(QLocale::English, QLocale::Australia), 601, 175'266, "English (Australia)" }}, // en_AU
Dict{{ LWC(QLocale::English, QLocale::Canada), 602, 174'295, "English (Canada)" }}, // en_CA
Dict{{ LWC(QLocale::English, QLocale::UnitedKingdom), 603, 174'433, "English (United Kingdom)" }}, // en_GB
Dict{{ QLocale::Spanish, 604, 264'717, "\x45\x73\x70\x61\xc3\xb1\x6f\x6c" }}, // es_ES
Dict{{ QLocale::Estonian, 605, 757'394, "\x45\x65\x73\x74\x69" }}, // et_EE
Dict{{ QLocale::Persian, 606, 333'911, "\xd9\x81\xd8\xa7\xd8\xb1\xd8\xb3\xdb\x8c" }}, // fa_IR
Dict{{ QLocale::French, 607, 321'391, "\x46\x72\x61\x6e\xc3\xa7\x61\x69\x73" }}, // fr_FR
Dict{{ QLocale::Hebrew, 608, 622'550, "\xd7\xa2\xd7\x91\xd7\xa8\xd7\x99\xd7\xaa" }}, // he_IL
Dict{{ QLocale::Hindi, 609, 56'105, "\xe0\xa4\xb9\xe0\xa4\xbf\xe0\xa4\xa8\xe0\xa5\x8d\xe0\xa4\xa6\xe0\xa5\x80" }}, // hi_IN
Dict{{ QLocale::Croatian, 610, 668'876, "\x48\x72\x76\x61\x74\x73\x6b\x69" }}, // hr_HR
Dict{{ QLocale::Hungarian, 611, 660'402, "\x4d\x61\x67\x79\x61\x72" }}, // hu_HU
Dict{{ QLocale::Armenian, 612, 928'746, "\xd5\x80\xd5\xa1\xd5\xb5\xd5\xa5\xd6\x80\xd5\xa5\xd5\xb6" }}, // hy_AM
Dict{{ QLocale::Indonesian, 613, 100'134, "\x49\x6e\x64\x6f\x6e\x65\x73\x69\x61" }}, // id_ID
Dict{{ QLocale::Italian, 614, 324'613, "\x49\x74\x61\x6c\x69\x61\x6e\x6f" }}, // it_IT
Dict{{ QLocale::Korean, 615, 1'256'987, "\xed\x95\x9c\xea\xb5\xad\xec\x96\xb4" }}, // ko_KR
Dict{{ QLocale::Lithuanian, 616, 267'427, "\x4c\x69\x65\x74\x75\x76\x69\xc5\xb3" }}, // lt_LT
Dict{{ QLocale::Latvian, 617, 641'602, "\x4c\x61\x74\x76\x69\x65\xc5\xa1\x75" }}, // lv_LV
Dict{{ QLocale::NorwegianBokmal, 618, 588'650, "\x4e\x6f\x72\x73\x6b" }}, // nb_NO
Dict{{ QLocale::Dutch, 619, 743'406, "\x4e\x65\x64\x65\x72\x6c\x61\x6e\x64\x73" }}, // nl_NL
Dict{{ QLocale::Polish, 620, 1'015'747, "\x50\x6f\x6c\x73\x6b\x69" }}, // pl_PL
Dict{{ QLocale::Portuguese, 621, 1'231'999, "\x50\x6f\x72\x74\x75\x67\x75\xc3\xaa\x73 (Brazil)" }}, // pt_BR
Dict{{ LWC(QLocale::Portuguese, QLocale::Portugal), 622, 138'571, "\x50\x6f\x72\x74\x75\x67\x75\xc3\xaa\x73" }}, // pt_PT
Dict{{ QLocale::Romanian, 623, 455'643, "\x52\x6f\x6d\xc3\xa2\x6e\xc4\x83" }}, // ro_RO
Dict{{ QLocale::Russian, 624, 463'194, "\xd0\xa0\xd1\x83\xd1\x81\xd1\x81\xd0\xba\xd0\xb8\xd0\xb9" }}, // ru_RU
Dict{{ QLocale::Slovak, 625, 525'328, "\x53\x6c\x6f\x76\x65\x6e\xc4\x8d\x69\x6e\x61" }}, // sk_SK
Dict{{ QLocale::Slovenian, 626, 1'143'710, "\x53\x6c\x6f\x76\x65\x6e\xc5\xa1\xc4\x8d\x69\x6e\x61" }}, // sl_SI
Dict{{ QLocale::Albanian, 627, 583'412, "\x53\x68\x71\x69\x70" }}, // sq_AL
Dict{{ QLocale::Swedish, 628, 593'877, "\x53\x76\x65\x6e\x73\x6b\x61" }}, // sv_SE
Dict{{ QLocale::Tamil, 629, 323'193, "\xe0\xae\xa4\xe0\xae\xae\xe0\xae\xbf\xe0\xae\xb4\xe0\xaf\x8d" }}, // ta_IN
Dict{{ QLocale::Tajik, 630, 369'931, "\xd0\xa2\xd0\xbe\xd2\xb7\xd0\xb8\xd0\xba\xd3\xa3" }}, // tg_TG
Dict{{ QLocale::Turkish, 631, 4'301'099, "\x54\xc3\xbc\x72\x6b\xc3\xa7\x65" }}, // tr_TR
Dict{{ QLocale::Ukrainian, 632, 445'711, "\xd0\xa3\xd0\xba\xd1\x80\xd0\xb0\xd1\x97\xd0\xbd\xd1\x81\xd1\x8c\xd0\xba\xd0\xb0" }}, // uk_UA
Dict{{ QLocale::Vietnamese, 633, 12'949, "\x54\x69\xe1\xba\xbf\x6e\x67\x20\x56\x69\xe1\xbb\x87\x74" }}, // vi_VN
// The Tajik code is 'tg_TG' in Chromium, but QT has only 'tg_TJ'.
};
inline auto IsSupportedLang(int lang) {
return ranges::contains(kDictionaries, lang, &Dict::id);
}
void EnsurePath() {
if (!QDir::current().mkpath(Spellchecker::DictionariesPath())) {
LOG(("App Error: Could not create dictionaries path."));
}
}
bool IsGoodPartName(const QString &name) {
return ranges::any_of(kDictExtensions, [&](const auto &ext) {
return name.endsWith(ext);
});
}
using DictLoaderPtr = std::shared_ptr<base::unique_qptr<DictLoader>>;
DictLoaderPtr BackgroundLoader;
rpl::event_stream<int> BackgroundLoaderChanged;
void SetBackgroundLoader(DictLoaderPtr loader) {
BackgroundLoader = std::move(loader);
}
void DownloadDictionaryInBackground(
not_null<Main::Session*> session,
int counter,
std::vector<int> langs) {
if (counter >= langs.size()) {
return;
}
const auto id = langs[counter];
counter++;
const auto destroyer = [=] {
BackgroundLoader = nullptr;
BackgroundLoaderChanged.fire(0);
if (DictionaryExists(id)) {
auto dicts = Core::App().settings().dictionariesEnabled();
if (!ranges::contains(dicts, id)) {
dicts.push_back(id);
Core::App().settings().setDictionariesEnabled(std::move(dicts));
Core::App().saveSettingsDelayed();
}
}
DownloadDictionaryInBackground(session, counter, langs);
};
if (DictionaryExists(id)) {
destroyer();
return;
}
auto sharedLoader = std::make_shared<base::unique_qptr<DictLoader>>();
*sharedLoader = base::make_unique_q<DictLoader>(
QCoreApplication::instance(),
session,
id,
GetDownloadLocation(id),
DictPathByLangId(id),
GetDownloadSize(id),
crl::guard(session, destroyer));
SetBackgroundLoader(std::move(sharedLoader));
BackgroundLoaderChanged.fire_copy(id);
}
void AddExceptions() {
const auto exceptions = ranges::views::all(
kExceptions
) | ranges::views::transform([](const auto &word) {
return word.utf16();
}) | ranges::views::filter([](const auto &word) {
return !(Platform::Spellchecker::IsWordInDictionary(word)
|| Spellchecker::IsWordSkippable(word));
}) | ranges::to_vector;
ranges::for_each(exceptions, Platform::Spellchecker::AddWord);
}
} // namespace
DictLoaderPtr GlobalLoader() {
return BackgroundLoader;
}
rpl::producer<int> GlobalLoaderChanged() {
return BackgroundLoaderChanged.events();
}
DictLoader::DictLoader(
QObject *parent,
not_null<Main::Session*> session,
int id,
MTP::DedicatedLoader::Location location,
const QString &folder,
int64 size,
Fn<void()> destroyCallback)
: BlobLoader(parent, session, id, location, folder, size)
, _destroyCallback(std::move(destroyCallback)) {
}
void DictLoader::unpack(const QString &path) {
crl::async([=] {
const auto success = Spellchecker::UnpackDictionary(path, id());
if (success) {
QFile(path).remove();
destroy();
return;
}
crl::on_main([=] { fail(); });
});
}
void DictLoader::destroy() {
Expects(_destroyCallback);
crl::on_main(_destroyCallback);
}
void DictLoader::fail() {
BlobLoader::fail();
destroy();
}
std::vector<Dict> Dictionaries() {
return kDictionaries | ranges::to_vector;
}
int64 GetDownloadSize(int id) {
return ranges::find(kDictionaries, id, &Spellchecker::Dict::id)->size;
}
MTP::DedicatedLoader::Location GetDownloadLocation(int id) {
const auto username = kCloudLocationUsername.utf16();
const auto i = ranges::find(kDictionaries, id, &Spellchecker::Dict::id);
return MTP::DedicatedLoader::Location{ username, i->postId };
}
QString DictPathByLangId(int langId) {
EnsurePath();
return u"%1/%2"_q.arg(
DictionariesPath(),
Spellchecker::LocaleFromLangId(langId).name());
}
QString DictionariesPath() {
return cWorkingDir() + u"tdata/dictionaries"_q;
}
bool UnpackDictionary(const QString &path, int langId) {
const auto folder = DictPathByLangId(langId);
return UnpackBlob(path, folder, IsGoodPartName);
}
bool DictionaryExists(int langId) {
if (!langId) {
return true;
}
const auto folder = DictPathByLangId(langId) + '/';
return ranges::none_of(kDictExtensions, [&](const auto &ext) {
const auto name = Spellchecker::LocaleFromLangId(langId).name();
return !QFile(folder + name + '.' + ext).exists();
});
}
bool RemoveDictionary(int langId) {
if (!langId) {
return true;
}
const auto fileName = Spellchecker::LocaleFromLangId(langId).name();
const auto folder = u"%1/%2/"_q.arg(
DictionariesPath(),
fileName);
return QDir(folder).removeRecursively();
}
bool WriteDefaultDictionary() {
// This is an unused function.
const auto en = QLocale::English;
if (DictionaryExists(en)) {
return false;
}
const auto fileName = QLocale(en).name();
const auto folder = u"%1/%2/"_q.arg(
DictionariesPath(),
fileName);
QDir(folder).removeRecursively();
const auto path = folder + fileName;
QDir().mkpath(folder);
auto input = QFile(u":/misc/en_US_dictionary"_q);
auto output = QFile(path);
if (input.open(QIODevice::ReadOnly)
&& output.open(QIODevice::WriteOnly)) {
output.write(input.readAll());
const auto result = Spellchecker::UnpackDictionary(path, en);
output.remove();
return result;
}
return false;
}
rpl::producer<QString> ButtonManageDictsState(
not_null<Main::Session*> session) {
if (Platform::Spellchecker::IsSystemSpellchecker()) {
return rpl::single(QString());
}
const auto computeString = [=] {
if (!Core::App().settings().spellcheckerEnabled()) {
return QString();
}
if (!Core::App().settings().dictionariesEnabled().size()) {
return QString();
}
const auto dicts = Core::App().settings().dictionariesEnabled();
const auto filtered = ranges::views::all(
dicts
) | ranges::views::filter(
DictionaryExists
) | ranges::to_vector;
const auto active = Platform::Spellchecker::ActiveLanguages();
return (active.size() == filtered.size())
? QString::number(filtered.size())
: tr::lng_contacts_loading(tr::now);
};
return rpl::single(
computeString()
) | rpl::then(
rpl::merge(
Spellchecker::SupportedScriptsChanged(),
Core::App().settings().dictionariesEnabledChanges(
) | rpl::to_empty,
Core::App().settings().spellcheckerEnabledChanges(
) | rpl::to_empty
) | rpl::map(computeString)
);
}
std::vector<int> DefaultLanguages() {
std::vector<int> langs;
const auto append = [&](const auto loc) {
const auto l = LanguageFromLocale(loc);
if (!ranges::contains(langs, l) && IsSupportedLang(l)) {
langs.push_back(l);
}
};
const auto method = QGuiApplication::inputMethod();
langs.reserve(method ? 3 : 2);
if (method) {
append(method->locale());
}
append(QLocale(Platform::SystemLanguage()));
append(QLocale(Lang::LanguageIdOrDefault(Lang::Id())));
return langs;
}
void Start(not_null<Main::Session*> session) {
Spellchecker::SetPhrases({ {
{ &ph::lng_spellchecker_submenu, tr::lng_spellchecker_submenu() },
{ &ph::lng_spellchecker_add, tr::lng_spellchecker_add() },
{ &ph::lng_spellchecker_remove, tr::lng_spellchecker_remove() },
{ &ph::lng_spellchecker_ignore, tr::lng_spellchecker_ignore() },
} });
const auto settings = &Core::App().settings();
auto &lifetime = session->lifetime();
const auto onEnabled = [=](auto enabled) {
Platform::Spellchecker::UpdateLanguages(
enabled
? settings->dictionariesEnabled()
: std::vector<int>());
};
const auto guard = gsl::finally([=] {
onEnabled(settings->spellcheckerEnabled());
});
if (Platform::Spellchecker::IsSystemSpellchecker()) {
Spellchecker::SupportedScriptsChanged()
| rpl::take(1)
| rpl::on_next(AddExceptions, lifetime);
return;
}
Spellchecker::SupportedScriptsChanged(
) | rpl::on_next(AddExceptions, lifetime);
Spellchecker::SetWorkingDirPath(DictionariesPath());
settings->dictionariesEnabledChanges(
) | rpl::on_next([](auto dictionaries) {
Platform::Spellchecker::UpdateLanguages(dictionaries);
}, lifetime);
settings->spellcheckerEnabledChanges(
) | rpl::on_next(onEnabled, lifetime);
const auto method = QGuiApplication::inputMethod();
const auto connectInput = [=] {
if (!method || !settings->spellcheckerEnabled()) {
return;
}
auto callback = [=] {
if (BackgroundLoader) {
return;
}
const auto l = LanguageFromLocale(method->locale());
if (!IsSupportedLang(l) || DictionaryExists(l)) {
return;
}
crl::on_main(session, [=] {
DownloadDictionaryInBackground(session, 0, { l });
});
};
QObject::connect(
method,
&QInputMethod::localeChanged,
std::move(callback));
};
if (settings->autoDownloadDictionaries()) {
session->data().contactsLoaded().changes(
) | rpl::on_next([=](bool loaded) {
if (!loaded) {
return;
}
DownloadDictionaryInBackground(session, 0, DefaultLanguages());
}, lifetime);
connectInput();
}
const auto disconnect = [=] {
QObject::disconnect(
method,
&QInputMethod::localeChanged,
nullptr,
nullptr);
};
lifetime.add([=] {
disconnect();
for (auto &[index, account] : session->domain().accounts()) {
if (const auto anotherSession = account->maybeSession()) {
if (anotherSession->uniqueId() != session->uniqueId()) {
Spellchecker::Start(anotherSession);
return;
}
}
}
});
rpl::combine(
settings->spellcheckerEnabledValue(),
settings->autoDownloadDictionariesValue()
) | rpl::on_next([=](bool spell, bool download) {
if (spell && download) {
connectInput();
return;
}
disconnect();
}, lifetime);
}
} // namespace Spellchecker
#endif // !TDESKTOP_DISABLE_SPELLCHECK

View File

@@ -0,0 +1,76 @@
/*
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
#ifndef TDESKTOP_DISABLE_SPELLCHECK
#include "storage/storage_cloud_blob.h"
#include "base/unique_qptr.h"
namespace Main {
class Session;
} // namespace Main
namespace Spellchecker {
struct Dict : public Storage::CloudBlob::Blob {
};
int64 GetDownloadSize(int id);
MTP::DedicatedLoader::Location GetDownloadLocation(int id);
[[nodiscard]] QString DictionariesPath();
[[nodiscard]] QString DictPathByLangId(int langId);
bool UnpackDictionary(const QString &path, int langId);
[[nodiscard]] bool DictionaryExists(int langId);
bool RemoveDictionary(int langId);
[[nodiscard]] bool IsEn(int langId);
bool WriteDefaultDictionary();
std::vector<Dict> Dictionaries();
void Start(not_null<Main::Session*> session);
[[nodiscard]] rpl::producer<QString> ButtonManageDictsState(
not_null<Main::Session*> session);
std::vector<int> DefaultLanguages();
class DictLoader : public Storage::CloudBlob::BlobLoader {
public:
DictLoader(
QObject *parent,
not_null<Main::Session*> session,
int id,
MTP::DedicatedLoader::Location location,
const QString &folder,
int64 size,
Fn<void()> destroyCallback);
void destroy() override;
rpl::lifetime &lifetime() {
return _lifetime;
}
private:
void unpack(const QString &path) override;
void fail() override;
// Be sure to always call it in the main thread.
Fn<void()> _destroyCallback;
rpl::lifetime _lifetime;
};
std::shared_ptr<base::unique_qptr<DictLoader>> GlobalLoader();
rpl::producer<int> GlobalLoaderChanged();
} // namespace Spellchecker
#endif // !TDESKTOP_DISABLE_SPELLCHECK

View File

@@ -0,0 +1,147 @@
/*
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/stickers_dice_pack.h"
#include "main/main_session.h"
#include "chat_helpers/stickers_lottie.h"
#include "data/data_session.h"
#include "data/data_document.h"
#include "base/unixtime.h"
#include "apiwrap.h"
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
namespace Stickers {
const QString DicePacks::kDiceString = QString::fromUtf8("\xF0\x9F\x8E\xB2");
const QString DicePacks::kDartString = QString::fromUtf8("\xF0\x9F\x8E\xAF");
const QString DicePacks::kSlotString = QString::fromUtf8("\xF0\x9F\x8E\xB0");
const QString DicePacks::kFballString = QString::fromUtf8("\xE2\x9A\xBD");
const QString DicePacks::kBballString = QString::fromUtf8("\xF0\x9F\x8F\x80");
const QString DicePacks::kPartyPopper = QString::fromUtf8("\xf0\x9f\x8e\x89");
DicePack::DicePack(not_null<Main::Session*> session, const QString &emoji)
: _session(session)
, _emoji(emoji) {
}
DicePack::~DicePack() = default;
DocumentData *DicePack::lookup(int value) {
if (!_requestId && _emoji != DicePacks::kPartyPopper) {
load();
}
tryGenerateLocalZero();
const auto i = _map.find(value);
return (i != end(_map)) ? i->second.get() : nullptr;
}
void DicePack::load() {
if (_requestId) {
return;
}
_requestId = _session->api().request(MTPmessages_GetStickerSet(
MTP_inputStickerSetDice(MTP_string(_emoji)),
MTP_int(0) // hash
)).done([=](const MTPmessages_StickerSet &result) {
result.match([&](const MTPDmessages_stickerSet &data) {
applySet(data);
}, [](const MTPDmessages_stickerSetNotModified &) {
LOG(("API Error: Unexpected messages.stickerSetNotModified."));
});
}).fail([=] {
_requestId = 0;
}).send();
}
void DicePack::applySet(const MTPDmessages_stickerSet &data) {
const auto isSlotMachine = DicePacks::IsSlot(_emoji);
auto index = 0;
auto documents = base::flat_map<DocumentId, not_null<DocumentData*>>();
for (const auto &sticker : data.vdocuments().v) {
const auto document = _session->data().processDocument(sticker);
if (document->sticker()) {
if (isSlotMachine) {
_map.emplace(index++, document);
} else {
documents.emplace(document->id, document);
}
}
}
if (isSlotMachine) {
return;
}
for (const auto &pack : data.vpacks().v) {
pack.match([&](const MTPDstickerPack &data) {
const auto emoji = qs(data.vemoticon());
if (emoji.isEmpty()) {
return;
}
const auto ch = int(emoji[0].unicode());
const auto index = (ch == '#') ? 0 : (ch + 1 - '1');
if (index < 0 || index > 6) {
return;
}
for (const auto &id : data.vdocuments().v) {
if (const auto document = documents.take(id.v)) {
_map.emplace(index, *document);
}
}
});
}
}
void DicePack::tryGenerateLocalZero() {
if (!_map.empty()) {
return;
}
const auto generateLocal = [&](int index, const QString &name) {
_map.emplace(
index,
ChatHelpers::GenerateLocalTgsSticker(_session, name));
};
if (_emoji == DicePacks::kDiceString) {
generateLocal(0, u"dice_idle"_q);
} else if (_emoji == DicePacks::kDartString) {
generateLocal(0, u"dart_idle"_q);
} else if (_emoji == DicePacks::kBballString) {
generateLocal(0, u"bball_idle"_q);
} else if (_emoji == DicePacks::kFballString) {
generateLocal(0, u"fball_idle"_q);
} else if (_emoji == DicePacks::kSlotString) {
generateLocal(0, u"slot_back"_q);
generateLocal(2, u"slot_pull"_q);
generateLocal(8, u"slot_0_idle"_q);
generateLocal(14, u"slot_1_idle"_q);
generateLocal(20, u"slot_2_idle"_q);
} else if (_emoji == DicePacks::kPartyPopper) {
generateLocal(0, u"winners"_q);
}
}
DicePacks::DicePacks(not_null<Main::Session*> session)
: _session(session) {
}
DocumentData *DicePacks::lookup(const QString &emoji, int value) {
const auto key = emoji.endsWith(QChar(0xFE0F))
? emoji.mid(0, emoji.size() - 1)
: emoji;
const auto i = _packs.find(key);
if (i != end(_packs)) {
return i->second->lookup(value);
}
return _packs.emplace(
key,
std::make_unique<DicePack>(_session, key)
).first->second->lookup(value);
}
} // namespace Stickers

View File

@@ -0,0 +1,61 @@
/*
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 DocumentData;
namespace Main {
class Session;
} // namespace Main
namespace Stickers {
class DicePack final {
public:
DicePack(not_null<Main::Session*> session, const QString &emoji);
~DicePack();
[[nodiscard]] DocumentData *lookup(int value);
private:
void load();
void applySet(const MTPDmessages_stickerSet &data);
void tryGenerateLocalZero();
const not_null<Main::Session*> _session;
QString _emoji;
base::flat_map<int, not_null<DocumentData*>> _map;
mtpRequestId _requestId = 0;
};
class DicePacks final {
public:
explicit DicePacks(not_null<Main::Session*> session);
static const QString kDiceString;
static const QString kDartString;
static const QString kSlotString;
static const QString kFballString;
static const QString kBballString;
static const QString kPartyPopper;
[[nodiscard]] static bool IsSlot(const QString &emoji) {
return (emoji == kSlotString);
}
[[nodiscard]] DocumentData *lookup(const QString &emoji, int value);
private:
const not_null<Main::Session*> _session;
base::flat_map<QString, std::unique_ptr<DicePack>> _packs;
};
} // namespace Stickers

View File

@@ -0,0 +1,99 @@
/*
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/stickers_emoji_image_loader.h"
#include "styles/style_chat.h"
#include <QtCore/QtMath>
namespace Stickers {
EmojiImageLoader::EmojiImageLoader(crl::weak_on_queue<EmojiImageLoader> weak)
: _weak(std::move(weak)) {
}
void EmojiImageLoader::init(
std::shared_ptr<UniversalImages> images,
bool largeEnabled) {
Expects(images != nullptr);
_images = std::move(images);
if (largeEnabled) {
_images->ensureLoaded();
}
}
QImage EmojiImageLoader::prepare(EmojiPtr emoji) const {
const auto loaded = _images->ensureLoaded();
const auto factor = style::DevicePixelRatio();
const auto side = st::largeEmojiSize + 2 * st::largeEmojiOutline;
auto tinted = QImage(
QSize(st::largeEmojiSize, st::largeEmojiSize) * factor,
QImage::Format_ARGB32_Premultiplied);
tinted.fill(Qt::white);
if (loaded) {
QPainter p(&tinted);
p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
_images->draw(
p,
emoji,
st::largeEmojiSize * factor,
0,
0);
}
auto result = QImage(
QSize(side, side) * factor,
QImage::Format_ARGB32_Premultiplied);
result.fill(Qt::transparent);
if (loaded) {
QPainter p(&result);
const auto delta = st::largeEmojiOutline * factor;
const auto planar = std::array<QPoint, 4>{ {
{ 0, -1 },
{ -1, 0 },
{ 1, 0 },
{ 0, 1 },
} };
for (const auto &shift : planar) {
for (auto i = 0; i != delta; ++i) {
p.drawImage(QPoint(delta, delta) + shift * (i + 1), tinted);
}
}
const auto diagonal = std::array<QPoint, 4>{ {
{ -1, -1 },
{ 1, -1 },
{ -1, 1 },
{ 1, 1 },
} };
const auto corrected = int(base::SafeRound(delta / M_SQRT2));
for (const auto &shift : diagonal) {
for (auto i = 0; i != corrected; ++i) {
p.drawImage(QPoint(delta, delta) + shift * (i + 1), tinted);
}
}
_images->draw(
p,
emoji,
st::largeEmojiSize * factor,
delta,
delta);
}
return result;
}
void EmojiImageLoader::switchTo(std::shared_ptr<UniversalImages> images) {
_images = std::move(images);
}
auto EmojiImageLoader::releaseImages() -> std::shared_ptr<UniversalImages> {
return std::exchange(
_images,
std::make_shared<UniversalImages>(_images->id()));
}
} // namespace Stickers

View File

@@ -0,0 +1,35 @@
/*
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 <crl/crl_object_on_queue.h>
#include "ui/emoji_config.h"
namespace Stickers {
class EmojiImageLoader {
public:
using UniversalImages = Ui::Emoji::UniversalImages;
explicit EmojiImageLoader(crl::weak_on_queue<EmojiImageLoader> weak);
void init(
std::shared_ptr<UniversalImages> images,
bool largeEnabled);
[[nodiscard]] QImage prepare(EmojiPtr emoji) const;
void switchTo(std::shared_ptr<UniversalImages> images);
std::shared_ptr<UniversalImages> releaseImages();
private:
crl::weak_on_queue<EmojiImageLoader> _weak;
std::shared_ptr<UniversalImages> _images;
};
} // namespace Stickers

View File

@@ -0,0 +1,532 @@
/*
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/stickers_emoji_pack.h"
#include "chat_helpers/stickers_emoji_image_loader.h"
#include "history/view/history_view_element.h"
#include "history/history_item.h"
#include "history/history.h"
#include "lottie/lottie_common.h"
#include "ui/emoji_config.h"
#include "ui/text/text_isolated_emoji.h"
#include "ui/image/image.h"
#include "ui/rect.h"
#include "main/main_session.h"
#include "data/data_file_origin.h"
#include "data/data_session.h"
#include "data/data_document.h"
#include "data/stickers/data_custom_emoji.h"
#include "core/core_settings.h"
#include "core/application.h"
#include "base/call_delayed.h"
#include "chat_helpers/stickers_lottie.h"
#include "history/view/media/history_view_sticker.h"
#include "lottie/lottie_single_player.h"
#include "apiwrap.h"
#include "styles/style_chat.h"
#include <QtCore/QBuffer>
namespace Stickers {
namespace {
constexpr auto kRefreshTimeout = 7200 * crl::time(1000);
constexpr auto kEmojiCachesCount = 4;
constexpr auto kPremiumCachesCount = 8;
[[nodiscard]] std::optional<int> IndexFromEmoticon(const QString &emoticon) {
if (emoticon.size() < 2) {
return std::nullopt;
}
const auto first = emoticon[0].unicode();
return (first >= '1' && first <= '9')
? std::make_optional(first - '1')
: (first == 55357 && emoticon[1].unicode() == 56607)
? std::make_optional(9)
: std::nullopt;
}
[[nodiscard]] QSize SingleSize() {
const auto single = st::largeEmojiSize;
const auto outline = st::largeEmojiOutline;
return Size(2 * outline + single) * style::DevicePixelRatio();
}
[[nodiscard]] const Lottie::ColorReplacements *ColorReplacements(int index) {
Expects(index >= 1 && index <= 5);
static const auto color1 = Lottie::ColorReplacements{
.modifier = Lottie::SkinModifier::Color1,
.tag = 1,
};
static const auto color2 = Lottie::ColorReplacements{
.modifier = Lottie::SkinModifier::Color2,
.tag = 2,
};
static const auto color3 = Lottie::ColorReplacements{
.modifier = Lottie::SkinModifier::Color3,
.tag = 3,
};
static const auto color4 = Lottie::ColorReplacements{
.modifier = Lottie::SkinModifier::Color4,
.tag = 4,
};
static const auto color5 = Lottie::ColorReplacements{
.modifier = Lottie::SkinModifier::Color5,
.tag = 5,
};
static const auto list = std::array{
&color1,
&color2,
&color3,
&color4,
&color5,
};
return list[index - 1];
}
} // namespace
QSize LargeEmojiImage::Size() {
return SingleSize();
}
EmojiPack::EmojiPack(not_null<Main::Session*> session)
: _session(session) {
refresh();
session->data().viewRemoved(
) | rpl::filter([](not_null<const ViewElement*> view) {
return view->isIsolatedEmoji() || view->isOnlyCustomEmoji();
}) | rpl::on_next([=](not_null<const ViewElement*> item) {
remove(item);
}, _lifetime);
Core::App().settings().largeEmojiChanges(
) | rpl::on_next([=](bool large) {
refreshAll();
}, _lifetime);
Ui::Emoji::Updated(
) | rpl::on_next([=] {
_images.clear();
refreshAll();
}, _lifetime);
}
EmojiPack::~EmojiPack() = default;
bool EmojiPack::add(not_null<ViewElement*> view) {
if (const auto custom = view->onlyCustomEmoji()) {
_onlyCustomItems.emplace(view);
return true;
} else if (const auto emoji = view->isolatedEmoji()) {
_items[emoji].emplace(view);
return true;
}
return false;
}
void EmojiPack::remove(not_null<const ViewElement*> view) {
Expects(view->isIsolatedEmoji() || view->isOnlyCustomEmoji());
if (view->isOnlyCustomEmoji()) {
_onlyCustomItems.remove(view);
} else if (const auto emoji = view->isolatedEmoji()) {
const auto i = _items.find(emoji);
Assert(i != end(_items));
const auto j = i->second.find(view);
Assert(j != end(i->second));
i->second.erase(j);
if (i->second.empty()) {
_items.erase(i);
}
}
}
auto EmojiPack::stickerForEmoji(EmojiPtr emoji) -> Sticker {
Expects(emoji != nullptr);
const auto i = _map.find(emoji);
if (i != end(_map)) {
return { i->second.get(), nullptr };
}
if (!emoji->colored()) {
return {};
}
const auto j = _map.find(emoji->original());
if (j != end(_map)) {
const auto index = emoji->variantIndex(emoji);
return { j->second.get(), ColorReplacements(index) };
}
return {};
}
auto EmojiPack::stickerForEmoji(const IsolatedEmoji &emoji) -> Sticker {
Expects(!emoji.empty());
if (!v::is_null(emoji.items[1])) {
return {};
} else if (const auto regular = std::get_if<EmojiPtr>(&emoji.items[0])) {
return stickerForEmoji(*regular);
}
return {};
}
std::shared_ptr<LargeEmojiImage> EmojiPack::image(EmojiPtr emoji) {
const auto i = _images.emplace(
emoji,
std::weak_ptr<LargeEmojiImage>()).first;
if (const auto result = i->second.lock()) {
return result;
}
auto result = std::make_shared<LargeEmojiImage>();
const auto raw = result.get();
const auto weak = base::make_weak(_session);
raw->load = [=] {
Core::App().emojiImageLoader().with([=](
const EmojiImageLoader &loader) {
crl::on_main(weak, [
=,
image = loader.prepare(emoji)
]() mutable {
const auto i = _images.find(emoji);
if (i != end(_images)) {
if (const auto strong = i->second.lock()) {
if (!strong->image) {
strong->load = nullptr;
strong->image.emplace(std::move(image));
_session->notifyDownloaderTaskFinished();
}
}
}
});
});
raw->load = nullptr;
};
i->second = result;
return result;
}
EmojiPtr EmojiPack::chooseInteractionEmoji(
not_null<HistoryItem*> item) const {
return chooseInteractionEmoji(item->originalText().text);
}
EmojiPtr EmojiPack::chooseInteractionEmoji(
const QString &emoticon) const {
const auto emoji = Ui::Emoji::Find(emoticon);
if (!emoji) {
return nullptr;
}
if (!animationsForEmoji(emoji).empty()) {
return emoji;
}
if (const auto original = emoji->original(); original != emoji) {
if (!animationsForEmoji(original).empty()) {
return original;
}
}
static const auto kHearts = {
QString::fromUtf8("\xf0\x9f\x92\x9b"),
QString::fromUtf8("\xf0\x9f\x92\x99"),
QString::fromUtf8("\xf0\x9f\x92\x9a"),
QString::fromUtf8("\xf0\x9f\x92\x9c"),
QString::fromUtf8("\xf0\x9f\xa7\xa1"),
QString::fromUtf8("\xf0\x9f\x96\xa4"),
QString::fromUtf8("\xf0\x9f\xa4\x8e"),
QString::fromUtf8("\xf0\x9f\xa4\x8d"),
};
return ranges::contains(kHearts, emoji->id())
? Ui::Emoji::Find(QString::fromUtf8("\xe2\x9d\xa4"))
: emoji;
}
auto EmojiPack::animationsForEmoji(EmojiPtr emoji) const
-> const base::flat_map<int, not_null<DocumentData*>> & {
static const auto empty = base::flat_map<int, not_null<DocumentData*>>();
if (!emoji) {
return empty;
}
const auto i = _animations.find(emoji);
return (i != end(_animations)) ? i->second : empty;
}
bool EmojiPack::hasAnimationsFor(not_null<HistoryItem*> item) const {
return !animationsForEmoji(chooseInteractionEmoji(item)).empty();
}
bool EmojiPack::hasAnimationsFor(const QString &emoticon) const {
return !animationsForEmoji(chooseInteractionEmoji(emoticon)).empty();
}
std::unique_ptr<Lottie::SinglePlayer> EmojiPack::effectPlayer(
not_null<DocumentData*> document,
QByteArray data,
QString filepath,
EffectType type) {
// Shortened copy from stickers_lottie module.
const auto baseKey = document->bigFileBaseCacheKey();
const auto tag = uint8(type);
const auto keyShift = ((tag << 4) & 0xF0)
| (uint8(ChatHelpers::StickerLottieSize::EmojiInteraction) & 0x0F);
const auto key = Storage::Cache::Key{
baseKey.high,
baseKey.low + keyShift
};
const auto get = [=](int i, FnMut<void(QByteArray &&cached)> handler) {
document->owner().cacheBigFile().get(
{ key.high, key.low + i },
std::move(handler));
};
const auto weak = base::make_weak(&document->session());
const auto put = [=](int i, QByteArray &&cached) {
crl::on_main(weak, [=, data = std::move(cached)]() mutable {
weak->data().cacheBigFile().put(
{ key.high, key.low + i },
std::move(data));
});
};
const auto size = (type == EffectType::PremiumSticker)
? HistoryView::Sticker::PremiumEffectSize(document)
: (type == EffectType::EmojiInteraction)
? HistoryView::Sticker::EmojiEffectSize()
: HistoryView::Sticker::MessageEffectSize();
const auto request = Lottie::FrameRequest{
size * style::DevicePixelRatio(),
};
auto &weakProvider = _sharedProviders[{ document, type }];
auto shared = [&] {
if (const auto result = weakProvider.lock()) {
return result;
}
const auto count = (type == EffectType::PremiumSticker)
? kPremiumCachesCount
: kEmojiCachesCount;
const auto result = Lottie::SinglePlayer::SharedProvider(
count,
get,
put,
Lottie::ReadContent(data, filepath),
request,
Lottie::Quality::High);
weakProvider = result;
return result;
}();
return std::make_unique<Lottie::SinglePlayer>(std::move(shared), request);
}
void EmojiPack::refresh() {
if (_requestId) {
return;
}
_requestId = _session->api().request(MTPmessages_GetStickerSet(
MTP_inputStickerSetAnimatedEmoji(),
MTP_int(0) // hash
)).done([=](const MTPmessages_StickerSet &result) {
_requestId = 0;
refreshAnimations();
result.match([&](const MTPDmessages_stickerSet &data) {
applySet(data);
}, [](const MTPDmessages_stickerSetNotModified &) {
LOG(("API Error: Unexpected messages.stickerSetNotModified."));
});
}).fail([=](const MTP::Error &error) {
_requestId = 0;
refreshDelayed();
}).send();
}
void EmojiPack::refreshAnimations() {
if (_animationsRequestId) {
return;
}
_animationsRequestId = _session->api().request(MTPmessages_GetStickerSet(
MTP_inputStickerSetAnimatedEmojiAnimations(),
MTP_int(0) // hash
)).done([=](const MTPmessages_StickerSet &result) {
_animationsRequestId = 0;
refreshDelayed();
result.match([&](const MTPDmessages_stickerSet &data) {
applyAnimationsSet(data);
}, [](const MTPDmessages_stickerSetNotModified &) {
LOG(("API Error: Unexpected messages.stickerSetNotModified."));
});
}).fail([=] {
_animationsRequestId = 0;
refreshDelayed();
}).send();
}
void EmojiPack::applySet(const MTPDmessages_stickerSet &data) {
const auto stickers = collectStickers(data.vdocuments().v);
auto was = base::take(_map);
for (const auto &pack : data.vpacks().v) {
pack.match([&](const MTPDstickerPack &data) {
applyPack(data, stickers);
});
}
for (const auto &[emoji, document] : _map) {
const auto i = was.find(emoji);
if (i == end(was)) {
refreshItems(emoji);
} else {
if (i->second != document) {
refreshItems(i->first);
}
was.erase(i);
}
}
for (const auto &[emoji, document] : was) {
refreshItems(emoji);
}
_refreshed.fire({});
}
void EmojiPack::applyAnimationsSet(const MTPDmessages_stickerSet &data) {
const auto stickers = collectStickers(data.vdocuments().v);
const auto &packs = data.vpacks().v;
const auto indices = collectAnimationsIndices(packs);
_animations.clear();
for (const auto &pack : packs) {
pack.match([&](const MTPDstickerPack &data) {
const auto emoticon = qs(data.vemoticon());
if (IndexFromEmoticon(emoticon).has_value()) {
return;
}
const auto emoji = Ui::Emoji::Find(emoticon);
if (!emoji) {
return;
}
for (const auto &id : data.vdocuments().v) {
const auto i = indices.find(id.v);
if (i == end(indices)) {
continue;
}
const auto j = stickers.find(id.v);
if (j == end(stickers)) {
continue;
}
for (const auto index : i->second) {
_animations[emoji].emplace(index, j->second);
}
}
});
}
++_animationsVersion;
}
auto EmojiPack::collectAnimationsIndices(
const QVector<MTPStickerPack> &packs
) const -> base::flat_map<uint64, base::flat_set<int>> {
auto result = base::flat_map<uint64, base::flat_set<int>>();
for (const auto &pack : packs) {
pack.match([&](const MTPDstickerPack &data) {
if (const auto index = IndexFromEmoticon(qs(data.vemoticon()))) {
for (const auto &id : data.vdocuments().v) {
result[id.v].emplace(*index);
}
}
});
}
return result;
}
void EmojiPack::refreshAll() {
auto items = base::flat_set<not_null<HistoryItem*>>();
auto count = 0;
for (const auto &[emoji, list] : _items) {
// refreshItems(list); // This call changes _items!
count += int(list.size());
}
items.reserve(count);
for (const auto &[emoji, list] : _items) {
// refreshItems(list); // This call changes _items!
for (const auto &view : list) {
items.emplace(view->data());
}
}
refreshItems(items);
refreshItems(_onlyCustomItems);
}
void EmojiPack::refreshItems(EmojiPtr emoji) {
const auto i = _items.find(IsolatedEmoji{ { emoji } });
if (!emoji->colored()) {
if (const auto count = emoji->variantsCount()) {
for (auto i = 0; i != count; ++i) {
refreshItems(emoji->variant(i + 1));
}
}
}
if (i == end(_items)) {
return;
}
refreshItems(i->second);
}
void EmojiPack::refreshItems(
const base::flat_set<not_null<ViewElement*>> &list) {
auto items = base::flat_set<not_null<HistoryItem*>>();
items.reserve(list.size());
for (const auto &view : list) {
items.emplace(view->data());
}
refreshItems(items);
}
void EmojiPack::refreshItems(
const base::flat_set<not_null<HistoryItem*>> &items) {
for (const auto &item : items) {
_session->data().requestItemViewRefresh(item);
}
}
void EmojiPack::applyPack(
const MTPDstickerPack &data,
const base::flat_map<uint64, not_null<DocumentData*>> &map) {
const auto emoji = [&] {
return Ui::Emoji::Find(qs(data.vemoticon()));
}();
const auto document = [&]() -> DocumentData * {
for (const auto &id : data.vdocuments().v) {
const auto i = map.find(id.v);
if (i != end(map)) {
return i->second.get();
}
}
return nullptr;
}();
if (emoji && document) {
_map.emplace_or_assign(emoji, document);
}
}
base::flat_map<uint64, not_null<DocumentData*>> EmojiPack::collectStickers(
const QVector<MTPDocument> &list) const {
auto result = base::flat_map<uint64, not_null<DocumentData*>>();
for (const auto &sticker : list) {
const auto document = _session->data().processDocument(
sticker);
if (document->sticker()) {
result.emplace(document->id, document);
}
}
return result;
}
void EmojiPack::refreshDelayed() {
base::call_delayed(kRefreshTimeout, _session, [=] {
refresh();
});
}
} // namespace Stickers

View File

@@ -0,0 +1,165 @@
/*
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/text/text_isolated_emoji.h"
#include "ui/image/image.h"
#include "base/timer.h"
#include <crl/crl_object_on_queue.h>
class HistoryItem;
class DocumentData;
namespace Main {
class Session;
} // namespace Main
namespace Lottie {
class SinglePlayer;
class FrameProvider;
struct ColorReplacements;
} // namespace Lottie
namespace Ui {
namespace Text {
class String;
} // namespace Text
namespace Emoji {
class UniversalImages;
} // namespace Emoji
} // namespace Ui
namespace HistoryView {
class Element;
} // namespace HistoryView
namespace Stickers {
using IsolatedEmoji = Ui::Text::IsolatedEmoji;
struct LargeEmojiImage {
std::optional<Image> image;
FnMut<void()> load;
[[nodiscard]] static QSize Size();
};
enum class EffectType : uint8 {
EmojiInteraction,
PremiumSticker,
MessageEffect,
};
class EmojiPack final {
public:
using ViewElement = HistoryView::Element;
struct Sticker {
DocumentData *document = nullptr;
const Lottie::ColorReplacements *replacements = nullptr;
[[nodiscard]] bool empty() const {
return (document == nullptr);
}
[[nodiscard]] explicit operator bool() const {
return !empty();
}
};
explicit EmojiPack(not_null<Main::Session*> session);
~EmojiPack();
bool add(not_null<ViewElement*> view);
void remove(not_null<const ViewElement*> view);
[[nodiscard]] Sticker stickerForEmoji(EmojiPtr emoji);
[[nodiscard]] Sticker stickerForEmoji(const IsolatedEmoji &emoji);
[[nodiscard]] std::shared_ptr<LargeEmojiImage> image(EmojiPtr emoji);
[[nodiscard]] EmojiPtr chooseInteractionEmoji(
not_null<HistoryItem*> item) const;
[[nodiscard]] EmojiPtr chooseInteractionEmoji(
const QString &emoticon) const;
[[nodiscard]] auto animationsForEmoji(EmojiPtr emoji) const
-> const base::flat_map<int, not_null<DocumentData*>> &;
[[nodiscard]] bool hasAnimationsFor(not_null<HistoryItem*> item) const;
[[nodiscard]] bool hasAnimationsFor(const QString &emoticon) const;
[[nodiscard]] int animationsVersion() const {
return _animationsVersion;
}
[[nodiscard]] rpl::producer<> refreshed() const {
return _refreshed.events();
}
[[nodiscard]] std::unique_ptr<Lottie::SinglePlayer> effectPlayer(
not_null<DocumentData*> document,
QByteArray data,
QString filepath,
EffectType type);
private:
class ImageLoader;
struct ProviderKey {
not_null<DocumentData*> document;
Stickers::EffectType type = {};
friend inline auto operator<=>(
const ProviderKey &,
const ProviderKey &) = default;
friend inline bool operator==(
const ProviderKey &,
const ProviderKey &) = default;
};
void refresh();
void refreshDelayed();
void refreshAnimations();
void applySet(const MTPDmessages_stickerSet &data);
void applyPack(
const MTPDstickerPack &data,
const base::flat_map<uint64, not_null<DocumentData*>> &map);
void applyAnimationsSet(const MTPDmessages_stickerSet &data);
[[nodiscard]] auto collectStickers(const QVector<MTPDocument> &list) const
-> base::flat_map<uint64, not_null<DocumentData*>>;
[[nodiscard]] auto collectAnimationsIndices(
const QVector<MTPStickerPack> &packs) const
-> base::flat_map<uint64, base::flat_set<int>>;
void refreshAll();
void refreshItems(EmojiPtr emoji);
void refreshItems(const base::flat_set<not_null<ViewElement*>> &list);
void refreshItems(const base::flat_set<not_null<HistoryItem*>> &items);
const not_null<Main::Session*> _session;
base::flat_map<EmojiPtr, not_null<DocumentData*>> _map;
base::flat_map<
IsolatedEmoji,
base::flat_set<not_null<HistoryView::Element*>>> _items;
base::flat_map<EmojiPtr, std::weak_ptr<LargeEmojiImage>> _images;
mtpRequestId _requestId = 0;
base::flat_set<not_null<HistoryView::Element*>> _onlyCustomItems;
int _animationsVersion = 0;
base::flat_map<
EmojiPtr,
base::flat_map<int, not_null<DocumentData*>>> _animations;
mtpRequestId _animationsRequestId = 0;
base::flat_map<
ProviderKey,
std::weak_ptr<Lottie::FrameProvider>> _sharedProviders;
rpl::event_stream<> _refreshed;
rpl::lifetime _lifetime;
};
} // namespace Stickers

View File

@@ -0,0 +1,153 @@
/*
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/stickers_gift_box_pack.h"
#include "apiwrap.h"
#include "data/data_document.h"
#include "data/data_file_origin.h"
#include "data/data_session.h"
#include "main/main_session.h"
namespace Stickers {
GiftBoxPack::GiftBoxPack(not_null<Main::Session*> session)
: _session(session) {
_premium.dividers = { 1, 3, 6, 12, 24 };
_ton.dividers = { 0, 10, 50 };
}
GiftBoxPack::~GiftBoxPack() = default;
rpl::producer<> GiftBoxPack::updated() const {
return _premium.updated.events();
}
rpl::producer<> GiftBoxPack::tonUpdated() const {
return _ton.updated.events();
}
int GiftBoxPack::monthsForStars(int stars) const {
if (stars <= 1000) {
return 3;
} else if (stars < 2500) {
return 6;
} else {
return 12;
}
}
DocumentData *GiftBoxPack::lookup(int months) const {
return lookup(_premium, months, false);
}
DocumentData *GiftBoxPack::tonLookup(int amount) const {
return lookup(_ton, amount, true);
}
DocumentData *GiftBoxPack::lookup(
const Pack &pack,
int divider,
bool exact) const {
const auto it = ranges::lower_bound(pack.dividers, divider);
const auto fallback = pack.documents.empty()
? nullptr
: pack.documents.front();
if (it == begin(pack.dividers)) {
return fallback;
} else if (it == end(pack.dividers)) {
return pack.documents.back();
}
const auto shift = exact
? ((*it > divider) ? 1 : 0)
: (std::abs(divider - (*(it - 1))) < std::abs(divider - (*it)))
? -1
: 0;
const auto index = int(std::distance(begin(pack.dividers), it - shift));
return (index >= pack.documents.size())
? fallback
: pack.documents[index];
}
Data::FileOrigin GiftBoxPack::origin() const {
return Data::FileOriginStickerSet(_premium.id, _premium.accessHash);
}
Data::FileOrigin GiftBoxPack::tonOrigin() const {
return Data::FileOriginStickerSet(_ton.id, _ton.accessHash);
}
void GiftBoxPack::load() {
load(_premium, MTP_inputStickerSetPremiumGifts());
}
void GiftBoxPack::tonLoad() {
load(_ton, MTP_inputStickerSetTonGifts());
}
void GiftBoxPack::load(Pack &pack, const MTPInputStickerSet &set) {
if (pack.requestId || !pack.documents.empty()) {
return;
}
pack.requestId = _session->api().request(MTPmessages_GetStickerSet(
set,
MTP_int(0) // Hash.
)).done([=, &pack](const MTPmessages_StickerSet &result) {
pack.requestId = 0;
result.match([&](const MTPDmessages_stickerSet &data) {
applySet(pack, data);
}, [](const MTPDmessages_stickerSetNotModified &) {
LOG(("API Error: Unexpected messages.stickerSetNotModified."));
});
}).fail([=, &pack] {
pack.requestId = 0;
}).send();
}
void GiftBoxPack::applySet(Pack &pack, const MTPDmessages_stickerSet &data) {
pack.id = data.vset().data().vid().v;
pack.accessHash = data.vset().data().vaccess_hash().v;
auto documents = base::flat_map<DocumentId, not_null<DocumentData*>>();
for (const auto &sticker : data.vdocuments().v) {
const auto document = _session->data().processDocument(sticker);
if (document->sticker()) {
documents.emplace(document->id, document);
if (pack.documents.empty()) {
// Fallback.
pack.documents.resize(1);
pack.documents[0] = document;
}
}
}
for (const auto &info : data.vpacks().v) {
const auto &data = info.data();
const auto emoji = qs(data.vemoticon());
if (emoji.isEmpty()) {
return;
}
for (const auto &id : data.vdocuments().v) {
if (const auto document = documents.take(id.v)) {
if (const auto sticker = (*document)->sticker()) {
if (!sticker->alt.isEmpty()) {
const auto ch = int(sticker->alt[0].unicode());
const auto index = (ch - '1'); // [0, 4];
if (index < 0 || index >= pack.dividers.size()) {
return;
}
if ((index + 1) > pack.documents.size()) {
pack.documents.resize((index + 1));
}
pack.documents[index] = (*document);
}
}
}
}
}
pack.updated.fire({});
}
} // namespace Stickers

View File

@@ -0,0 +1,66 @@
/*
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 DocumentData;
namespace Data {
struct FileOrigin;
} // namespace Data
namespace Main {
class Session;
} // namespace Main
namespace Stickers {
class GiftBoxPack final {
public:
explicit GiftBoxPack(not_null<Main::Session*> session);
~GiftBoxPack();
void load();
[[nodiscard]] int monthsForStars(int stars) const;
[[nodiscard]] DocumentData *lookup(int months) const;
[[nodiscard]] Data::FileOrigin origin() const;
[[nodiscard]] rpl::producer<> updated() const;
void tonLoad();
[[nodiscard]] DocumentData *tonLookup(int amount) const;
[[nodiscard]] Data::FileOrigin tonOrigin() const;
[[nodiscard]] rpl::producer<> tonUpdated() const;
private:
using SetId = uint64;
struct Pack {
SetId id = 0;
uint64 accessHash = 0;
std::vector<DocumentData*> documents;
mtpRequestId requestId = 0;
std::vector<int> dividers;
rpl::event_stream<> updated;
};
void load(Pack &pack, const MTPInputStickerSet &set);
void applySet(Pack &pack, const MTPDmessages_stickerSet &data);
[[nodiscard]] DocumentData *lookup(
const Pack &pack,
int divider,
bool exact) const;
const not_null<Main::Session*> _session;
const std::vector<int> _localMonths;
const std::vector<int> _localTonAmounts;
Pack _premium;
Pack _ton;
};
} // namespace Stickers

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,343 @@
/*
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 "chat_helpers/compose/compose_features.h"
#include "chat_helpers/tabbed_selector.h"
#include "media/clip/media_clip_reader.h"
#include "mtproto/sender.h"
#include "ui/dpr/dpr_image.h"
#include "ui/round_rect.h"
#include "ui/userpic_view.h"
namespace Ui {
class InputField;
class CrossButton;
} // namespace Ui
namespace Ui::Text {
class CustomEmoji;
} // namespace Ui::Text
namespace Data {
class StickersSet;
class StickersSetThumbnailView;
class DocumentMedia;
} // namespace Data
namespace Lottie {
class SinglePlayer;
class FrameRenderer;
} // namespace Lottie
namespace Window {
class SessionController;
} // namespace Window
namespace style {
struct EmojiPan;
} // namespace style
namespace ChatHelpers {
enum class ValidateIconAnimations {
Full,
Scroll,
None,
};
[[nodiscard]] uint64 EmojiSectionSetId(Ui::Emoji::Section section);
[[nodiscard]] uint64 RecentEmojiSectionSetId();
[[nodiscard]] uint64 AllEmojiSectionSetId();
[[nodiscard]] uint64 SearchEmojiSectionSetId();
[[nodiscard]] std::optional<Ui::Emoji::Section> SetIdEmojiSection(uint64 id);
struct GifSection {
DocumentData *document = nullptr;
EmojiPtr emoji;
friend inline constexpr auto operator<=>(
GifSection,
GifSection) = default;
};
[[nodiscard]] rpl::producer<std::vector<GifSection>> GifSectionsValue(
not_null<Main::Session*> session);
[[nodiscard]] std::vector<EmojiPtr> SearchEmoji(
const std::vector<QString> &query,
base::flat_set<EmojiPtr> &outResultSet);
struct StickerIcon {
explicit StickerIcon(uint64 setId);
StickerIcon(
not_null<Data::StickersSet*> set,
DocumentData *sticker,
int pixw,
int pixh);
StickerIcon(StickerIcon&&);
StickerIcon &operator=(StickerIcon&&);
~StickerIcon();
void ensureMediaCreated() const;
uint64 setId = 0;
Data::StickersSet *set = nullptr;
mutable std::unique_ptr<Lottie::SinglePlayer> lottie;
mutable std::unique_ptr<Ui::Text::CustomEmoji> custom;
mutable Media::Clip::ReaderPointer webm;
mutable QImage savedFrame;
DocumentData *sticker = nullptr;
ChannelData *megagroup = nullptr;
mutable std::shared_ptr<Data::StickersSetThumbnailView> thumbnailMedia;
mutable std::shared_ptr<Data::DocumentMedia> stickerMedia;
mutable Ui::PeerUserpicView megagroupUserpic;
int pixw = 0;
int pixh = 0;
mutable rpl::lifetime lifetime;
};
class GradientPremiumStar {
public:
GradientPremiumStar();
[[nodiscard]] QImage image() const;
private:
void renderOnDemand() const;
mutable QImage _image;
rpl::lifetime _lifetime;
};
class StickersListFooter final : public TabbedSelector::InnerFooter {
public:
struct Descriptor {
not_null<Main::Session*> session;
Fn<QColor()> customTextColor;
Fn<bool()> paused;
not_null<RpWidget*> parent;
const style::EmojiPan *st = nullptr;
ComposeFeatures features;
bool forceFirstFrame = false;
};
explicit StickersListFooter(Descriptor &&descriptor);
void preloadImages();
void validateSelectedIcon(
uint64 setId,
ValidateIconAnimations animations);
void refreshIcons(
std::vector<StickerIcon> icons,
uint64 activeSetId,
Fn<std::shared_ptr<Lottie::FrameRenderer>()> renderer,
ValidateIconAnimations animations);
void leaveToChildEvent(QEvent *e, QWidget *child) override;
void clearHeavyData();
[[nodiscard]] rpl::producer<uint64> setChosen() const;
[[nodiscard]] rpl::producer<> openSettingsRequests() const;
void paintExpanding(
Painter &p,
QRect clip,
float64 radius,
RectPart origin);
[[nodiscard]] static int IconFrameSize();
protected:
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
bool eventHook(QEvent *e) override;
void processHideFinished() override;
private:
enum class SpecialOver {
None,
Settings,
};
struct IconId {
int index = 0;
int subindex = 0;
friend inline bool operator==(IconId a, IconId b) {
return (a.index == b.index) && (a.subindex == b.subindex);
}
};
using OverState = std::variant<SpecialOver, IconId>;
struct IconInfo {
int index = 0;
int left = 0;
int adjustedLeft = 0;
int width = 0;
bool visible = false;
};
struct ScrollState {
template <typename UpdateCallback>
explicit ScrollState(UpdateCallback &&callback);
bool animationCallback(crl::time now);
int selected = 0;
int max = 0;
int draggingStartX = 0;
bool dragging = false;
anim::value x;
anim::value selectionX;
anim::value selectionWidth;
crl::time animationStart = 0;
Ui::Animations::Basic animation;
};
struct ExpandingContext {
QRect clip;
float64 progress = 0.;
int radius = 0;
bool expanding = false;
};
void enumerateVisibleIcons(Fn<void(const IconInfo &)> callback) const;
void enumerateIcons(Fn<bool(const IconInfo &)> callback) const;
void enumerateSubicons(Fn<bool(const IconInfo &)> callback) const;
[[nodiscard]] IconInfo iconInfo(int index) const;
[[nodiscard]] IconInfo subiconInfo(int index) const;
[[nodiscard]] std::shared_ptr<Lottie::FrameRenderer> getLottieRenderer();
void setSelectedIcon(
int newSelected,
ValidateIconAnimations animations);
void setSelectedSubicon(
int newSelected,
ValidateIconAnimations animations);
void validateIconLottieAnimation(const StickerIcon &icon);
void validateIconWebmAnimation(const StickerIcon &icon);
void validateIconAnimation(const StickerIcon &icon);
void customEmojiRepaint();
void refreshIconsGeometry(
uint64 activeSetId,
ValidateIconAnimations animations);
void refreshSubiconsGeometry();
void refreshScrollableDimensions();
void updateSelected();
void updateSetIcon(uint64 setId);
void updateSetIconAt(int left);
void checkDragging(ScrollState &state);
bool finishDragging(ScrollState &state);
bool finishDragging();
void paint(Painter &p, const ExpandingContext &context) const;
void paintStickerSettingsIcon(QPainter &p) const;
void paintSetIcon(
Painter &p,
const ExpandingContext &context,
const IconInfo &info,
crl::time now,
bool paused) const;
void prepareSetIcon(
const ExpandingContext &context,
const IconInfo &info,
crl::time now,
bool paused) const;
void paintSetIconToCache(
Painter &p,
const ExpandingContext &context,
const IconInfo &info,
crl::time now,
bool paused) const;
void paintSelectionBg(
QPainter &p,
const ExpandingContext &context) const;
void paintLeftRightFading(
QPainter &p,
const ExpandingContext &context) const;
void updateEmojiSectionWidth();
void updateEmojiWidthCallback();
void scrollByWheelEvent(not_null<QWheelEvent*> e);
void validateFadeLeft(int leftWidth) const;
void validateFadeRight(int rightWidth) const;
void validateFadeMask() const;
void clipCallback(Media::Clip::Notification notification, uint64 setId);
const not_null<Main::Session*> _session;
const Fn<QColor()> _customTextColor;
const Fn<bool()> _paused;
const ComposeFeatures _features;
static constexpr auto kVisibleIconsCount = 8;
std::weak_ptr<Lottie::FrameRenderer> _lottieRenderer;
std::vector<StickerIcon> _icons;
Fn<std::shared_ptr<Lottie::FrameRenderer>()> _renderer;
uint64 _activeByScrollId = 0;
OverState _selected = SpecialOver::None;
OverState _pressed = SpecialOver::None;
QPoint _iconsMousePos, _iconsMouseDown;
int _iconsLeft = 0;
int _iconsRight = 0;
int _iconsTop = 0;
int _singleWidth = 0;
QPoint _areaPosition;
mutable QImage _fadeLeftCache;
mutable QColor _fadeLeftColor;
mutable QImage _fadeRightCache;
mutable QColor _fadeRightColor;
mutable QImage _fadeMask;
mutable QImage _setIconCache;
ScrollState _iconState;
ScrollState _subiconState;
Ui::RoundRect _selectionBg, _subselectionBg;
Ui::Animations::Simple _subiconsWidthAnimation;
int _subiconsWidth = 0;
bool _subiconsExpanded = false;
bool _repaintScheduled = false;
bool _forceFirstFrame = false;
rpl::event_stream<> _openSettingsRequests;
rpl::event_stream<uint64> _setChosen;
};
class LocalStickersManager final {
public:
explicit LocalStickersManager(not_null<Main::Session*> session);
void install(uint64 setId);
[[nodiscard]] bool isInstalledLocally(uint64 setId) const;
void removeInstalledLocally(uint64 setId);
bool clearInstalledLocally();
private:
void sendInstallRequest(
uint64 setId,
const MTPInputStickerSet &input);
void installedLocally(uint64 setId);
void notInstalledLocally(uint64 setId);
const not_null<Main::Session*> _session;
MTP::Sender _api;
base::flat_set<uint64> _installedLocallySets;
};
} // namespace ChatHelpers

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,462 @@
/*
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 "chat_helpers/compose/compose_features.h"
#include "chat_helpers/tabbed_selector.h"
#include "data/stickers/data_stickers.h"
#include "ui/round_rect.h"
#include "base/variant.h"
#include "base/timer.h"
class StickerPremiumMark;
namespace Main {
class Session;
} // namespace Main
namespace Window {
class SessionController;
} // namespace Window
namespace Ui {
class LinkButton;
class PopupMenu;
class RippleAnimation;
class BoxContent;
class PathShiftGradient;
class TabbedSearch;
} // namespace Ui
namespace Lottie {
class Animation;
class MultiPlayer;
class FrameRenderer;
} // namespace Lottie
namespace Data {
class DocumentMedia;
class StickersSet;
} // namespace Data
namespace Media::Clip {
class ReaderPointer;
enum class Notification;
} // namespace Media::Clip
namespace style {
struct EmojiPan;
struct FlatLabel;
} // namespace style
namespace ChatHelpers {
struct StickerIcon;
enum class ValidateIconAnimations;
class StickersListFooter;
class LocalStickersManager;
enum class StickersListMode {
Full,
Masks,
UserpicBuilder,
ChatIntro,
MessageEffects,
};
struct StickerCustomRecentDescriptor {
not_null<DocumentData*> document;
QString cornerEmoji;
};
struct StickersListDescriptor {
std::shared_ptr<Show> show;
StickersListMode mode = StickersListMode::Full;
Fn<bool()> paused;
std::vector<StickerCustomRecentDescriptor> customRecentList;
const style::EmojiPan *st = nullptr;
ComposeFeatures features;
};
class StickersListWidget final : public TabbedSelector::Inner {
public:
using Mode = StickersListMode;
StickersListWidget(
QWidget *parent,
not_null<Window::SessionController*> controller,
PauseReason level,
Mode mode = Mode::Full);
StickersListWidget(
QWidget *parent,
StickersListDescriptor &&descriptor);
rpl::producer<FileChosen> chosen() const;
rpl::producer<> scrollUpdated() const;
rpl::producer<TabbedSelector::Action> choosingUpdated() const;
void refreshRecent() override;
void preloadImages() override;
void clearSelection() override;
object_ptr<TabbedSelector::InnerFooter> createFooter() override;
void showStickerSet(uint64 setId);
void showMegagroupSet(ChannelData *megagroup);
void afterShown() override;
void beforeHiding() override;
void refreshStickers();
std::vector<StickerIcon> fillIcons();
uint64 currentSet(int yOffset) const;
void sendSearchRequest();
void searchForSets(const QString &query, std::vector<EmojiPtr> emoji);
std::shared_ptr<Lottie::FrameRenderer> getLottieRenderer();
base::unique_qptr<Ui::PopupMenu> fillContextMenu(
const SendMenu::Details &details) override;
bool mySetsEmpty() const;
void applySearchQuery(std::vector<QString> &&query);
[[nodiscard]] rpl::producer<int> recentShownCount() const;
~StickersListWidget();
protected:
void visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void paintEvent(QPaintEvent *e) override;
void leaveEventHook(QEvent *e) override;
void leaveToChildEvent(QEvent *e, QWidget *child) override;
void enterFromChildEvent(QEvent *e, QWidget *child) override;
TabbedSelector::InnerFooter *getFooter() const override;
void processHideFinished() override;
void processPanelHideFinished() override;
int countDesiredHeight(int newWidth) override;
private:
struct Sticker;
struct Set;
enum class Section {
Featured,
Stickers,
Search,
};
struct OverSticker {
int section = 0;
int index = 0;
bool overDelete = false;
inline bool operator==(OverSticker other) const {
return (section == other.section)
&& (index == other.index)
&& (overDelete == other.overDelete);
}
inline bool operator!=(OverSticker other) const {
return !(*this == other);
}
};
struct OverSet {
int section = 0;
inline bool operator==(OverSet other) const {
return (section == other.section);
}
inline bool operator!=(OverSet other) const {
return !(*this == other);
}
};
struct OverButton {
int section = 0;
inline bool operator==(OverButton other) const {
return (section == other.section);
}
inline bool operator!=(OverButton other) const {
return !(*this == other);
}
};
struct OverGroupAdd {
inline bool operator==(OverGroupAdd other) const {
return true;
}
inline bool operator!=(OverGroupAdd other) const {
return !(*this == other);
}
};
using OverState = std::variant<
v::null_t,
OverSticker,
OverSet,
OverButton,
OverGroupAdd>;
struct SectionInfo {
int section = 0;
int count = 0;
int top = 0;
int rowsCount = 0;
int rowsTop = 0;
int rowsBottom = 0;
};
struct FeaturedSet {
uint64 id = 0;
Data::StickersSetFlags flags;
std::vector<Sticker> stickers;
};
static std::vector<Sticker> PrepareStickers(
const QVector<DocumentData*> &pack,
bool skipPremium);
void setupSearch();
void preloadMoreOfficial();
QSize boundingBoxSize() const;
template <typename Callback>
bool enumerateSections(Callback callback) const;
SectionInfo sectionInfo(int section) const;
SectionInfo sectionInfoByOffset(int yOffset) const;
void setSection(Section section);
void displaySet(uint64 setId);
void removeMegagroupSet(bool locally);
void removeSet(uint64 setId);
void refreshMySets();
void refreshFeaturedSets();
void refreshSearchSets();
void refreshSearchIndex();
bool setHasTitle(const Set &set) const;
bool stickerHasDeleteButton(const Set &set, int index) const;
[[nodiscard]] std::vector<Sticker> collectRecentStickers();
[[nodiscard]] std::vector<Sticker> collectCustomRecents();
void refreshRecentStickers(bool resize = true);
void refreshEffects();
void refreshFavedStickers();
enum class GroupStickersPlace {
Visible,
Hidden,
};
void refreshMegagroupStickers(GroupStickersPlace place);
void refreshSettingsVisibility();
void updateSelected();
void setSelected(OverState newSelected);
void setPressed(OverState newPressed);
[[nodiscard]] std::unique_ptr<Ui::RippleAnimation> createButtonRipple(
int section);
[[nodiscard]] QPoint buttonRippleTopLeft(int section) const;
[[nodiscard]] std::vector<Set> &shownSets();
[[nodiscard]] const std::vector<Set> &shownSets() const;
[[nodiscard]] int featuredRowHeight() const;
void checkVisibleFeatured(int visibleTop, int visibleBottom);
void readVisibleFeatured(int visibleTop, int visibleBottom);
void paintStickers(Painter &p, QRect clip);
void paintMegagroupEmptySet(Painter &p, int y, bool buttonSelected);
void paintSticker(
Painter &p,
Set &set,
int y,
int section,
int index,
crl::time now,
bool paused,
bool selected,
bool deleteSelected);
void paintEmptySearchResults(Painter &p);
void ensureLottiePlayer(Set &set);
void setupLottie(Set &set, int section, int index);
void setupWebm(Set &set, int section, int index);
void clipCallback(
Media::Clip::Notification notification,
uint64 setId,
not_null<DocumentData*> document,
int indexHint);
[[nodiscard]] bool itemVisible(const SectionInfo &info, int index) const;
void markLottieFrameShown(Set &set);
void checkVisibleLottie();
void pauseInvisibleLottieIn(const SectionInfo &info);
void takeHeavyData(std::vector<Set> &to, std::vector<Set> &from);
void takeHeavyData(Set &to, Set &from);
void takeHeavyData(Sticker &to, Sticker &from);
void clearHeavyIn(Set &set, bool clearSavedFrames = true);
void clearHeavyData();
void updateItems();
void updateSets();
void repaintItems(crl::time now = 0);
void updateSet(const SectionInfo &info);
void repaintItems(
const SectionInfo &info,
crl::time now);
[[nodiscard]] int stickersRight() const;
[[nodiscard]] bool featuredHasAddButton(int index) const;
[[nodiscard]] QRect featuredAddRect(int index) const;
[[nodiscard]] QRect featuredAddRect(
const SectionInfo &info,
bool installedSet) const;
[[nodiscard]] bool hasRemoveButton(int index) const;
[[nodiscard]] QRect removeButtonRect(int index) const;
[[nodiscard]] QRect removeButtonRect(const SectionInfo &info) const;
[[nodiscard]] int megagroupSetInfoLeft() const;
void refreshMegagroupSetGeometry();
[[nodiscard]] QRect megagroupSetButtonRectFinal() const;
[[nodiscard]] const Data::StickersSetsOrder &defaultSetsOrder() const;
[[nodiscard]] Data::StickersSetsOrder &defaultSetsOrderRef();
void filterEffectsByEmoji(const std::vector<EmojiPtr> &emoji);
enum class AppendSkip {
None,
Archived,
Installed,
};
bool appendSet(
std::vector<Set> &to,
uint64 setId,
bool externalLayout,
AppendSkip skip = AppendSkip::None);
int stickersLeft() const;
QRect stickerRect(int section, int sel);
void removeRecentSticker(int section, int index);
void removeFavedSticker(int section, int index);
void setColumnCount(int count);
void refreshFooterIcons();
void refreshIcons(ValidateIconAnimations animations);
void showStickerSetBox(
not_null<DocumentData*> document,
uint64 setId);
void cancelSetsSearch();
void showSearchResults();
void searchResultsDone(const MTPmessages_FoundStickerSets &result);
void refreshSearchRows();
void refreshSearchRows(const std::vector<uint64> *cloudSets);
void fillFilteredStickersRow();
void fillLocalSearchRows(const QString &query);
void fillCloudSearchRows(const std::vector<uint64> &cloudSets);
void addSearchRow(not_null<Data::StickersSet*> set);
void toggleSearchLoading(bool loading);
void showPreview();
Ui::MessageSendingAnimationFrom messageSentAnimationInfo(
int section,
int index,
not_null<DocumentData*> document);
const Mode _mode;
const std::shared_ptr<Show> _show;
const ComposeFeatures _features;
Ui::RoundRect _overBg;
std::unique_ptr<Ui::TabbedSearch> _search;
MTP::Sender _api;
std::unique_ptr<LocalStickersManager> _localSetsManager;
ChannelData *_megagroupSet = nullptr;
uint64 _megagroupSetIdRequested = 0;
std::vector<StickerCustomRecentDescriptor> _customRecentIds;
std::vector<Set> _mySets;
std::vector<Set> _officialSets;
std::vector<Set> _searchSets;
int _featuredSetsCount = 0;
std::vector<bool> _custom;
std::vector<EmojiPtr> _cornerEmoji;
base::flat_set<not_null<DocumentData*>> _favedStickersMap;
std::weak_ptr<Lottie::FrameRenderer> _lottieRenderer;
bool _paintAsPremium = false;
bool _showingSetById = false;
crl::time _lastScrolledAt = 0;
crl::time _lastFullUpdatedAt = 0;
mtpRequestId _officialRequestId = 0;
int _officialOffset = 0;
Section _section = Section::Stickers;
const bool _isMasks;
const bool _isEffects;
base::Timer _updateItemsTimer;
base::Timer _updateSetsTimer;
base::flat_set<uint64> _repaintSetsIds;
StickersListFooter *_footer = nullptr;
int _rowsLeft = 0;
int _columnCount = 1;
QSize _singleSize;
OverState _selected;
OverState _pressed;
QPoint _lastMousePosition;
Ui::RoundRect _trendingAddBgOver, _trendingAddBg, _inactiveButtonBg;
Ui::RoundRect _groupCategoryAddBgOver, _groupCategoryAddBg;
const std::unique_ptr<Ui::PathShiftGradient> _pathGradient;
Ui::Text::String _megagroupSetAbout;
QString _megagroupSetButtonText;
int _megagroupSetButtonTextWidth = 0;
QRect _megagroupSetButtonRect;
std::unique_ptr<Ui::RippleAnimation> _megagroupSetButtonRipple;
QString _addText;
int _addWidth;
QString _installedText;
int _installedWidth;
object_ptr<Ui::LinkButton> _settings;
base::Timer _previewTimer;
bool _previewShown = false;
std::unique_ptr<StickerPremiumMark> _premiumMark;
std::vector<not_null<DocumentData*>> _filteredStickers;
std::vector<EmojiPtr> _filterStickersCornerEmoji;
rpl::variable<int> _recentShownCount;
std::map<QString, std::vector<uint64>> _searchCache;
std::vector<std::pair<uint64, QStringList>> _searchIndex;
base::Timer _searchRequestTimer;
QString _searchQuery, _searchNextQuery;
mtpRequestId _searchRequestId = 0;
rpl::event_stream<FileChosen> _chosen;
rpl::event_stream<> _scrollUpdated;
rpl::event_stream<TabbedSelector::Action> _choosingUpdated;
};
[[nodiscard]] object_ptr<Ui::BoxContent> MakeConfirmRemoveSetBox(
not_null<Main::Session*> session,
const style::FlatLabel &st,
uint64 setId);
} // namespace ChatHelpers

View File

@@ -0,0 +1,365 @@
/*
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/stickers_lottie.h"
#include "lottie/lottie_single_player.h"
#include "lottie/lottie_multi_player.h"
#include "data/stickers/data_stickers_set.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_session.h"
#include "data/data_file_origin.h"
#include "storage/cache/storage_cache_database.h"
#include "storage/localimageloader.h"
#include "history/view/media/history_view_media_common.h"
#include "media/clip/media_clip_reader.h"
#include "ui/chat/attach/attach_prepare.h"
#include "ui/effects/path_shift_gradient.h"
#include "ui/image/image_location_factory.h"
#include "ui/painter.h"
#include "main/main_session.h"
#include <xxhash.h>
namespace ChatHelpers {
namespace {
constexpr auto kDontCacheLottieAfterArea = 512 * 512;
[[nodiscard]] uint64 LocalStickerId(QStringView name) {
auto full = u"local_sticker:"_q;
full.append(name);
return XXH64(full.data(), full.size() * sizeof(QChar), 0);
}
} // namespace
uint8 LottieCacheKeyShift(uint8 replacementsTag, StickerLottieSize sizeTag) {
return ((replacementsTag << 4) & 0xF0) | (uint8(sizeTag) & 0x0F);
}
template <typename Method>
auto LottieCachedFromContent(
Method &&method,
Storage::Cache::Key baseKey,
uint8 keyShift,
not_null<Main::Session*> session,
const QByteArray &content,
QSize box) {
const auto key = Storage::Cache::Key{
baseKey.high,
baseKey.low + keyShift
};
const auto get = [=](FnMut<void(QByteArray &&cached)> handler) {
session->data().cacheBigFile().get(
key,
std::move(handler));
};
const auto weak = base::make_weak(session);
const auto put = [=](QByteArray &&cached) {
crl::on_main(weak, [=, data = std::move(cached)]() mutable {
weak->data().cacheBigFile().put(key, std::move(data));
});
};
return method(
get,
put,
content,
Lottie::FrameRequest{ box });
}
template <typename Method>
auto LottieFromDocument(
Method &&method,
not_null<Data::DocumentMedia*> media,
uint8 keyShift,
QSize box) {
const auto document = media->owner();
const auto data = media->bytes();
const auto filepath = document->filepath();
if (box.width() * box.height() > kDontCacheLottieAfterArea) {
// Don't use frame caching for large stickers.
return method(
Lottie::ReadContent(data, filepath),
Lottie::FrameRequest{ box });
}
if (const auto baseKey = document->bigFileBaseCacheKey()) {
return LottieCachedFromContent(
std::forward<Method>(method),
baseKey,
keyShift,
&document->session(),
Lottie::ReadContent(data, filepath),
box);
}
return method(
Lottie::ReadContent(data, filepath),
Lottie::FrameRequest{ box });
}
std::unique_ptr<Lottie::SinglePlayer> LottiePlayerFromDocument(
not_null<Data::DocumentMedia*> media,
StickerLottieSize sizeTag,
QSize box,
Lottie::Quality quality,
std::shared_ptr<Lottie::FrameRenderer> renderer) {
return LottiePlayerFromDocument(
media,
nullptr,
sizeTag,
box,
quality,
std::move(renderer));
}
std::unique_ptr<Lottie::SinglePlayer> LottiePlayerFromDocument(
not_null<Data::DocumentMedia*> media,
const Lottie::ColorReplacements *replacements,
StickerLottieSize sizeTag,
QSize box,
Lottie::Quality quality,
std::shared_ptr<Lottie::FrameRenderer> renderer) {
const auto method = [&](auto &&...args) {
return std::make_unique<Lottie::SinglePlayer>(
std::forward<decltype(args)>(args)...,
quality,
replacements,
std::move(renderer));
};
const auto keyShift = LottieCacheKeyShift(
replacements ? replacements->tag : uint8(0),
sizeTag);
return LottieFromDocument(method, media, uint8(keyShift), box);
}
not_null<Lottie::Animation*> LottieAnimationFromDocument(
not_null<Lottie::MultiPlayer*> player,
not_null<Data::DocumentMedia*> media,
StickerLottieSize sizeTag,
QSize box) {
const auto method = [&](auto &&...args) {
return player->append(std::forward<decltype(args)>(args)...);
};
return LottieFromDocument(method, media, uint8(sizeTag), box);
}
bool HasLottieThumbnail(
StickerType thumbType,
Data::StickersSetThumbnailView *thumb,
Data::DocumentMedia *media) {
if (thumb) {
return (thumbType == StickerType::Tgs)
&& !thumb->content().isEmpty();
} else if (!media) {
return false;
}
const auto document = media->owner();
if (const auto info = document->sticker()) {
if (!info->isLottie()) {
return false;
}
media->automaticLoad(document->stickerSetOrigin(), nullptr);
if (!media->loaded()) {
return false;
}
return document->bigFileBaseCacheKey().valid();
}
return false;
}
std::unique_ptr<Lottie::SinglePlayer> LottieThumbnail(
Data::StickersSetThumbnailView *thumb,
Data::DocumentMedia *media,
StickerLottieSize sizeTag,
QSize box,
std::shared_ptr<Lottie::FrameRenderer> renderer) {
const auto baseKey = thumb
? thumb->owner()->thumbnailBigFileBaseCacheKey()
: media
? media->owner()->bigFileBaseCacheKey()
: Storage::Cache::Key();
if (!baseKey) {
return nullptr;
}
const auto content = thumb
? thumb->content()
: Lottie::ReadContent(media->bytes(), media->owner()->filepath());
if (content.isEmpty()) {
return nullptr;
}
const auto method = [](auto &&...args) {
return std::make_unique<Lottie::SinglePlayer>(
std::forward<decltype(args)>(args)...);
};
const auto session = thumb
? &thumb->owner()->session()
: &media->owner()->session();
return LottieCachedFromContent(
method,
baseKey,
uint8(sizeTag),
session,
content,
box);
}
bool HasWebmThumbnail(
StickerType thumbType,
Data::StickersSetThumbnailView *thumb,
Data::DocumentMedia *media) {
if (thumb) {
return (thumbType == StickerType::Webm)
&& !thumb->content().isEmpty();
} else if (!media) {
return false;
}
const auto document = media->owner();
if (const auto info = document->sticker()) {
if (!info->isWebm()) {
return false;
}
media->automaticLoad(document->stickerSetOrigin(), nullptr);
if (!media->loaded()) {
return false;
}
return document->bigFileBaseCacheKey().valid();
}
return false;
}
Media::Clip::ReaderPointer WebmThumbnail(
Data::StickersSetThumbnailView *thumb,
Data::DocumentMedia *media,
Fn<void(Media::Clip::Notification)> callback) {
return thumb
? ::Media::Clip::MakeReader(
thumb->content(),
std::move(callback))
: ::Media::Clip::MakeReader(
media->owner()->location(),
media->bytes(),
std::move(callback));
}
bool PaintStickerThumbnailPath(
QPainter &p,
not_null<Data::DocumentMedia*> media,
QRect target,
QLinearGradient *gradient,
bool mirrorHorizontal) {
const auto &path = media->thumbnailPath();
const auto dimensions = media->owner()->dimensions;
if (path.isEmpty() || dimensions.isEmpty() || target.isEmpty()) {
return false;
}
p.save();
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.translate(target.topLeft());
if (gradient) {
const auto scale = dimensions.width() / float64(target.width());
const auto shift = p.worldTransform().dx();
gradient->setStart((gradient->start().x() - shift) * scale, 0);
gradient->setFinalStop(
(gradient->finalStop().x() - shift) * scale,
0);
p.setBrush(*gradient);
}
if (mirrorHorizontal) {
const auto c = QPointF(target.width() / 2., target.height() / 2.);
p.translate(c);
p.scale(-1., 1.);
p.translate(-c);
}
p.scale(
target.width() / float64(dimensions.width()),
target.height() / float64(dimensions.height()));
p.drawPath(path);
p.restore();
return true;
}
bool PaintStickerThumbnailPath(
QPainter &p,
not_null<Data::DocumentMedia*> media,
QRect target,
not_null<Ui::PathShiftGradient*> gradient,
bool mirrorHorizontal) {
return gradient->paint([&](const Ui::PathShiftGradient::Background &bg) {
if (const auto color = std::get_if<style::color>(&bg)) {
p.setBrush(*color);
return PaintStickerThumbnailPath(
p,
media,
target,
nullptr,
mirrorHorizontal);
}
const auto gradient = v::get<QLinearGradient*>(bg);
return PaintStickerThumbnailPath(
p,
media,
target,
gradient,
mirrorHorizontal);
});
}
QSize ComputeStickerSize(not_null<DocumentData*> document, QSize box) {
const auto sticker = document->sticker();
const auto dimensions = document->dimensions;
if (!sticker || !sticker->isLottie() || dimensions.isEmpty()) {
return HistoryView::DownscaledSize(dimensions, box);
}
const auto ratio = style::DevicePixelRatio();
const auto request = Lottie::FrameRequest{ box * ratio };
return HistoryView::NonEmptySize(request.size(dimensions, 8) / ratio);
}
not_null<DocumentData*> GenerateLocalSticker(
not_null<Main::Session*> session,
const QString &path) {
auto task = FileLoadTask(
session,
path,
QByteArray(),
nullptr,
nullptr,
SendMediaType::File,
FileLoadTo(0, {}, {}, 0),
{},
false,
nullptr,
LocalStickerId(path));
task.process({ .generateGoodThumbnail = false });
const auto result = task.peekResult();
Assert(result != nullptr);
const auto document = session->data().processDocument(
result->document,
Images::FromImageInMemory(
result->thumb,
"WEBP",
result->thumbbytes));
document->setLocation(Core::FileLocation(path));
Ensures(document->sticker());
return document;
}
not_null<DocumentData*> GenerateLocalTgsSticker(
not_null<Main::Session*> session,
const QString &name) {
const auto result = GenerateLocalSticker(
session,
u":/animations/"_q + name + u".tgs"_q);
Ensures(result->sticker()->isLottie());
return result;
}
} // namespace ChatHelpers

View File

@@ -0,0 +1,142 @@
/*
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
enum class StickerType : uchar;
namespace base {
template <typename Enum>
class Flags;
} // namespace base
namespace Storage {
namespace Cache {
struct Key;
} // namespace Cache
} // namespace Storage
namespace Media::Clip {
class ReaderPointer;
enum class Notification;
} // namespace Media::Clip
namespace Lottie {
class SinglePlayer;
class MultiPlayer;
class FrameRenderer;
class Animation;
enum class Quality : char;
struct ColorReplacements;
} // namespace Lottie
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class PathShiftGradient;
} // namespace Ui
namespace Data {
class DocumentMedia;
class StickersSetThumbnailView;
enum class StickersSetFlag : ushort;
using StickersSetFlags = base::flags<StickersSetFlag>;
} // namespace Data
namespace ChatHelpers {
enum class StickerLottieSize : uint8 {
MessageHistory,
StickerSet, // In Emoji used for forum topic profile cover icons.
StickersPanel,
StickersFooter,
SetsListThumbnail,
InlineResults,
EmojiInteraction,
EmojiInteractionReserved1,
EmojiInteractionReserved2,
EmojiInteractionReserved3,
EmojiInteractionReserved4,
EmojiInteractionReserved5,
EmojiInteractionReserved6,
EmojiInteractionReserved7,
ChatIntroHelloSticker,
StickerEmojiSize,
PinnedProfileUniqueGiftSize,
};
[[nodiscard]] uint8 LottieCacheKeyShift(
uint8 replacementsTag,
StickerLottieSize sizeTag);
[[nodiscard]] std::unique_ptr<Lottie::SinglePlayer> LottiePlayerFromDocument(
not_null<Data::DocumentMedia*> media,
StickerLottieSize sizeTag,
QSize box,
Lottie::Quality quality = Lottie::Quality(),
std::shared_ptr<Lottie::FrameRenderer> renderer = nullptr);
[[nodiscard]] std::unique_ptr<Lottie::SinglePlayer> LottiePlayerFromDocument(
not_null<Data::DocumentMedia*> media,
const Lottie::ColorReplacements *replacements,
StickerLottieSize sizeTag,
QSize box,
Lottie::Quality quality = Lottie::Quality(),
std::shared_ptr<Lottie::FrameRenderer> renderer = nullptr);
[[nodiscard]] not_null<Lottie::Animation*> LottieAnimationFromDocument(
not_null<Lottie::MultiPlayer*> player,
not_null<Data::DocumentMedia*> media,
StickerLottieSize sizeTag,
QSize box);
[[nodiscard]] bool HasLottieThumbnail(
StickerType thumbType,
Data::StickersSetThumbnailView *thumb,
Data::DocumentMedia *media);
[[nodiscard]] std::unique_ptr<Lottie::SinglePlayer> LottieThumbnail(
Data::StickersSetThumbnailView *thumb,
Data::DocumentMedia *media,
StickerLottieSize sizeTag,
QSize box,
std::shared_ptr<Lottie::FrameRenderer> renderer = nullptr);
[[nodiscard]] bool HasWebmThumbnail(
StickerType thumbType,
Data::StickersSetThumbnailView *thumb,
Data::DocumentMedia *media);
[[nodiscard]] Media::Clip::ReaderPointer WebmThumbnail(
Data::StickersSetThumbnailView *thumb,
Data::DocumentMedia *media,
Fn<void(Media::Clip::Notification)> callback);
bool PaintStickerThumbnailPath(
QPainter &p,
not_null<Data::DocumentMedia*> media,
QRect target,
QLinearGradient *gradient = nullptr,
bool mirrorHorizontal = false);
bool PaintStickerThumbnailPath(
QPainter &p,
not_null<Data::DocumentMedia*> media,
QRect target,
not_null<Ui::PathShiftGradient*> gradient,
bool mirrorHorizontal = false);
[[nodiscard]] QSize ComputeStickerSize(
not_null<DocumentData*> document,
QSize box);
[[nodiscard]] not_null<DocumentData*> GenerateLocalSticker(
not_null<Main::Session*> session,
const QString &path);
[[nodiscard]] not_null<DocumentData*> GenerateLocalTgsSticker(
not_null<Main::Session*> session,
const QString &name);
} // namespace ChatHelpers

View File

@@ -0,0 +1,520 @@
/*
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/tabbed_panel.h"
#include "ui/widgets/shadow.h"
#include "ui/image/image_prepare.h"
#include "ui/ui_utility.h"
#include "chat_helpers/tabbed_selector.h"
#include "window/window_session_controller.h"
#include "mainwindow.h"
#include "core/application.h"
#include "base/options.h"
#include "styles/style_chat_helpers.h"
namespace ChatHelpers {
namespace {
constexpr auto kHideTimeoutMs = 300;
constexpr auto kDelayedHideTimeoutMs = 3000;
base::options::toggle TabbedPanelShowOnClick({
.id = kOptionTabbedPanelShowOnClick,
.name = "Show tabbed panel by click",
.description = "Show Emoji / Stickers / GIFs panel only after a click.",
});
} // namespace
const char kOptionTabbedPanelShowOnClick[] = "tabbed-panel-show-on-click";
bool ShowPanelOnClick() {
return TabbedPanelShowOnClick.value();
}
TabbedPanel::TabbedPanel(
QWidget *parent,
not_null<Window::SessionController*> controller,
not_null<TabbedSelector*> selector)
: TabbedPanel(parent, {
.regularWindow = controller,
.nonOwnedSelector = selector,
}) {
}
TabbedPanel::TabbedPanel(
QWidget *parent,
not_null<Window::SessionController*> controller,
object_ptr<TabbedSelector> selector)
: TabbedPanel(parent, {
.regularWindow = controller,
.ownedSelector = std::move(selector),
}) {
}
TabbedPanel::TabbedPanel(
QWidget *parent,
TabbedPanelDescriptor &&descriptor)
: RpWidget(parent)
, _regularWindow(descriptor.regularWindow)
, _ownedSelector(std::move(descriptor.ownedSelector))
, _selector(descriptor.nonOwnedSelector
? descriptor.nonOwnedSelector
: _ownedSelector.data())
, _heightRatio(st::emojiPanHeightRatio)
, _minContentHeight(st::emojiPanMinHeight)
, _maxContentHeight(st::emojiPanMaxHeight) {
Expects(_selector != nullptr);
_selector->setParent(this);
_selector->setRoundRadius(st::emojiPanRadius);
_selector->setAfterShownCallback([=](SelectorTab tab) {
if (_regularWindow) {
_regularWindow->enableGifPauseReason(_selector->level());
}
_pauseAnimations.fire(true);
});
_selector->setBeforeHidingCallback([=](SelectorTab tab) {
if (_regularWindow) {
_regularWindow->disableGifPauseReason(_selector->level());
}
_pauseAnimations.fire(false);
});
_selector->showRequests(
) | rpl::on_next([=] {
showFromSelector();
}, lifetime());
resize(
QRect(0, 0, st::emojiPanWidth, st::emojiPanMaxHeight).marginsAdded(
innerPadding()).size());
_contentMaxHeight = st::emojiPanMaxHeight;
_contentHeight = _contentMaxHeight;
_selector->resize(st::emojiPanWidth, _contentHeight);
_selector->move(innerRect().topLeft());
_hideTimer.setCallback([this] { hideByTimerOrLeave(); });
_selector->checkForHide(
) | rpl::on_next([=] {
if (!rect().contains(mapFromGlobal(QCursor::pos()))) {
_hideTimer.callOnce(kDelayedHideTimeoutMs);
}
}, lifetime());
_selector->cancelled(
) | rpl::on_next([=] {
hideAnimated();
}, lifetime());
_selector->slideFinished(
) | rpl::on_next([=] {
InvokeQueued(this, [=] {
if (_hideAfterSlide) {
startOpacityAnimation(true);
}
});
}, lifetime());
macWindowDeactivateEvents(
) | rpl::filter([=] {
return !isHidden() && !preventAutoHide();
}) | rpl::on_next([=] {
hideAnimated();
}, lifetime());
setAttribute(Qt::WA_OpaquePaintEvent, false);
hideChildren();
hide();
}
not_null<TabbedSelector*> TabbedPanel::selector() const {
return _selector;
}
rpl::producer<bool> TabbedPanel::pauseAnimations() const {
return _pauseAnimations.events();
}
bool TabbedPanel::isSelectorStolen() const {
return (_selector->parent() != this);
}
void TabbedPanel::moveBottomRight(int bottom, int right) {
const auto isNew = (_bottom != bottom || _right != right);
_bottom = bottom;
_right = right;
// If the panel is already shown, update the position.
if (!isHidden() && isNew) {
moveHorizontally();
} else {
updateContentHeight();
}
}
void TabbedPanel::moveTopRight(int top, int right) {
const auto isNew = (_top != top || _right != right);
_top = top;
_right = right;
// If the panel is already shown, update the position.
if (!isHidden() && isNew) {
moveHorizontally();
} else {
updateContentHeight();
}
}
void TabbedPanel::setDesiredHeightValues(
float64 ratio,
int minHeight,
int maxHeight) {
_heightRatio = ratio;
_minContentHeight = minHeight;
_maxContentHeight = maxHeight;
updateContentHeight();
}
void TabbedPanel::setDropDown(bool dropDown) {
selector()->setDropDown(dropDown);
_dropDown = dropDown;
}
void TabbedPanel::updateContentHeight() {
auto addedHeight = innerPadding().top() + innerPadding().bottom();
auto marginsHeight = _selector->marginTop() + _selector->marginBottom();
auto availableHeight = _dropDown
? (parentWidget()->height() - _top - marginsHeight)
: (_bottom - marginsHeight);
auto wantedContentHeight = qRound(_heightRatio * availableHeight)
- addedHeight;
auto contentHeight = marginsHeight + std::clamp(
wantedContentHeight,
_minContentHeight,
_maxContentHeight);
auto resultTop = _dropDown
? _top
: (_bottom - addedHeight - contentHeight);
if (contentHeight == _contentHeight) {
move(x(), resultTop);
return;
}
_contentHeight = contentHeight;
resize(QRect(0, 0, innerRect().width(), _contentHeight).marginsAdded(innerPadding()).size());
move(x(), resultTop);
_selector->resize(innerRect().width(), _contentHeight);
update();
}
void TabbedPanel::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
// This call can finish _a_show animation and destroy _showAnimation.
auto opacityAnimating = _a_opacity.animating();
auto showAnimating = _a_show.animating();
if (_showAnimation && !showAnimating) {
_showAnimation.reset();
if (!opacityAnimating) {
showChildren();
_selector->afterShown();
}
}
if (showAnimating) {
Assert(_showAnimation != nullptr);
if (auto opacity = _a_opacity.value(_hiding ? 0. : 1.)) {
_showAnimation->paintFrame(p, 0, 0, width(), _a_show.value(1.), opacity);
}
} else if (opacityAnimating) {
p.setOpacity(_a_opacity.value(_hiding ? 0. : 1.));
p.drawPixmap(0, 0, _cache);
} else if (_hiding || isHidden()) {
hideFinished();
} else {
if (!_cache.isNull()) _cache = QPixmap();
Ui::Shadow::paint(p, innerRect(), width(), _selector->st().showAnimation.shadow);
}
}
void TabbedPanel::moveHorizontally() {
const auto padding = innerPadding();
const auto width = innerRect().width() + padding.left() + padding.right();
const auto right = std::max(
parentWidget()->width() - std::max(_right, width),
0);
moveToRight(right, y());
updateContentHeight();
}
void TabbedPanel::enterEventHook(QEnterEvent *e) {
Core::App().registerLeaveSubscription(this);
showAnimated();
}
bool TabbedPanel::preventAutoHide() const {
return _selector->preventAutoHide();
}
void TabbedPanel::leaveEventHook(QEvent *e) {
Core::App().unregisterLeaveSubscription(this);
if (preventAutoHide()) {
return;
}
if (_a_show.animating() || _a_opacity.animating()) {
hideAnimated();
} else {
_hideTimer.callOnce(kHideTimeoutMs);
}
return RpWidget::leaveEventHook(e);
}
void TabbedPanel::otherEnter() {
showAnimated();
}
void TabbedPanel::otherLeave() {
if (preventAutoHide()) {
return;
}
if (_a_opacity.animating()) {
hideByTimerOrLeave();
} else {
// In case of animations disabled add some delay before hiding.
// Otherwise if emoji suggestions panel is shown in between
// (z-order wise) the emoji toggle button and tabbed panel,
// we won't be able to move cursor from the button to the panel.
_hideTimer.callOnce(anim::Disabled() ? kHideTimeoutMs : 0);
}
}
void TabbedPanel::hideFast() {
if (isHidden()) return;
if (_selector && !_selector->isHidden()) {
_selector->beforeHiding();
}
_hideTimer.cancel();
_hiding = false;
_a_opacity.stop();
hideFinished();
}
void TabbedPanel::opacityAnimationCallback() {
update();
if (!_a_opacity.animating()) {
if (_hiding) {
_hiding = false;
hideFinished();
} else if (!_a_show.animating()) {
showChildren();
_selector->afterShown();
}
}
}
void TabbedPanel::hideByTimerOrLeave() {
if (isHidden() || preventAutoHide()) {
return;
}
hideAnimated();
}
void TabbedPanel::prepareCacheFor(bool hiding) {
if (_a_opacity.animating()) {
_hiding = hiding;
return;
}
auto showAnimation = base::take(_a_show);
auto showAnimationData = base::take(_showAnimation);
_hiding = false;
showChildren();
_cache = Ui::GrabWidget(this);
_a_show = base::take(showAnimation);
_showAnimation = base::take(showAnimationData);
_hiding = hiding;
if (_a_show.animating()) {
hideChildren();
}
}
void TabbedPanel::startOpacityAnimation(bool hiding) {
if (_selector && !_selector->isHidden()) {
_selector->beforeHiding();
}
prepareCacheFor(hiding);
hideChildren();
_a_opacity.start(
[=] { opacityAnimationCallback(); },
_hiding ? 1. : 0.,
_hiding ? 0. : 1.,
st::emojiPanDuration);
}
void TabbedPanel::startShowAnimation() {
if (!_a_show.animating()) {
auto image = grabForAnimation();
_showAnimation = std::make_unique<Ui::PanelAnimation>(
_selector->st().showAnimation,
(_dropDown
? Ui::PanelAnimation::Origin::TopRight
: Ui::PanelAnimation::Origin::BottomRight));
auto inner = rect().marginsRemoved(st::emojiPanMargins);
_showAnimation->setFinalImage(
std::move(image),
QRect(
inner.topLeft() * style::DevicePixelRatio(),
inner.size() * style::DevicePixelRatio()));
_showAnimation->setCornerMasks(Images::CornersMask(st::emojiPanRadius));
_showAnimation->start();
}
hideChildren();
_a_show.start([this] { update(); }, 0., 1., st::emojiPanShowDuration);
}
QImage TabbedPanel::grabForAnimation() {
auto cache = base::take(_cache);
auto opacityAnimation = base::take(_a_opacity);
auto showAnimationData = base::take(_showAnimation);
auto showAnimation = base::take(_a_show);
showChildren();
Ui::SendPendingMoveResizeEvents(this);
auto result = QImage(
size() * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(style::DevicePixelRatio());
result.fill(Qt::transparent);
if (_selector) {
QPainter p(&result);
Ui::RenderWidget(p, _selector, _selector->pos());
}
_a_show = base::take(showAnimation);
_showAnimation = base::take(showAnimationData);
_a_opacity = base::take(opacityAnimation);
_cache = base::take(cache);
return result;
}
void TabbedPanel::hideAnimated() {
if (isHidden() || _hiding) {
return;
}
_hideTimer.cancel();
if (_selector->isSliding()) {
_hideAfterSlide = true;
} else {
startOpacityAnimation(true);
}
// There is no reason to worry about the message scheduling box
// while it moves the user to the separate scheduled section.
_shouldFinishHide = _selector->hasMenu();
}
void TabbedPanel::toggleAnimated() {
if (isHidden() || _hiding || _hideAfterSlide) {
showAnimated();
} else {
hideAnimated();
}
}
void TabbedPanel::hideFinished() {
hide();
_a_show.stop();
_showAnimation.reset();
_cache = QPixmap();
_hiding = false;
_shouldFinishHide = false;
_selector->hideFinished();
}
void TabbedPanel::showAnimated() {
_hideTimer.cancel();
_hideAfterSlide = false;
showStarted();
}
void TabbedPanel::showStarted() {
if (_shouldFinishHide) {
return;
}
if (isHidden()) {
_selector->showStarted();
moveHorizontally();
raise();
show();
startShowAnimation();
} else if (_hiding) {
startOpacityAnimation(false);
}
}
bool TabbedPanel::eventFilter(QObject *obj, QEvent *e) {
if (TabbedPanelShowOnClick.value()) {
return false;
} else if (e->type() == QEvent::Enter) {
otherEnter();
} else if (e->type() == QEvent::Leave) {
otherLeave();
}
return false;
}
void TabbedPanel::showFromSelector() {
if (isHidden()) {
moveHorizontally();
startShowAnimation();
show();
}
showChildren();
showAnimated();
}
style::margins TabbedPanel::innerPadding() const {
return st::emojiPanMargins;
}
QRect TabbedPanel::innerRect() const {
return rect().marginsRemoved(innerPadding());
}
bool TabbedPanel::overlaps(const QRect &globalRect) const {
if (isHidden() || !_cache.isNull()) return false;
auto testRect = QRect(mapFromGlobal(globalRect.topLeft()), globalRect.size());
auto inner = rect().marginsRemoved(st::emojiPanMargins);
const auto radius = st::emojiPanRadius;
return inner.marginsRemoved(QMargins(radius, 0, radius, 0)).contains(testRect)
|| inner.marginsRemoved(QMargins(0, radius, 0, radius)).contains(testRect);
}
TabbedPanel::~TabbedPanel() {
hideFast();
if (!_ownedSelector && _regularWindow) {
_regularWindow->takeTabbedSelectorOwnershipFrom(this);
}
}
} // namespace ChatHelpers

View File

@@ -0,0 +1,133 @@
/*
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/effects/animations.h"
#include "ui/rp_widget.h"
#include "base/timer.h"
#include "base/object_ptr.h"
namespace Window {
class SessionController;
} // namespace Window
namespace Ui {
class PanelAnimation;
} // namespace Ui
namespace ChatHelpers {
class TabbedSelector;
extern const char kOptionTabbedPanelShowOnClick[];
[[nodiscard]] bool ShowPanelOnClick();
struct TabbedPanelDescriptor {
Window::SessionController *regularWindow = nullptr;
object_ptr<TabbedSelector> ownedSelector = { nullptr };
TabbedSelector *nonOwnedSelector = nullptr;
};
class TabbedPanel : public Ui::RpWidget {
public:
TabbedPanel(
QWidget *parent,
not_null<Window::SessionController*> controller,
not_null<TabbedSelector*> selector);
TabbedPanel(
QWidget *parent,
not_null<Window::SessionController*> controller,
object_ptr<TabbedSelector> selector);
TabbedPanel(QWidget *parent, TabbedPanelDescriptor &&descriptor);
[[nodiscard]] bool isSelectorStolen() const;
[[nodiscard]] not_null<TabbedSelector*> selector() const;
[[nodiscard]] rpl::producer<bool> pauseAnimations() const;
void moveBottomRight(int bottom, int right);
void moveTopRight(int top, int right);
void setDesiredHeightValues(
float64 ratio,
int minHeight,
int maxHeight);
void setDropDown(bool dropDown);
void hideFast();
bool hiding() const {
return _hiding || _hideTimer.isActive();
}
bool overlaps(const QRect &globalRect) const;
void showAnimated();
void hideAnimated();
void toggleAnimated();
~TabbedPanel();
protected:
void enterEventHook(QEnterEvent *e) override;
void leaveEventHook(QEvent *e) override;
void otherEnter();
void otherLeave();
void paintEvent(QPaintEvent *e) override;
bool eventFilter(QObject *obj, QEvent *e) override;
private:
void hideByTimerOrLeave();
void moveHorizontally();
void showFromSelector();
style::margins innerPadding() const;
// Rounded rect which has shadow around it.
QRect innerRect() const;
QImage grabForAnimation();
void startShowAnimation();
void startOpacityAnimation(bool hiding);
void prepareCacheFor(bool hiding);
void opacityAnimationCallback();
void hideFinished();
void showStarted();
bool preventAutoHide() const;
void updateContentHeight();
Window::SessionController * const _regularWindow = nullptr;
const object_ptr<TabbedSelector> _ownedSelector = { nullptr };
const not_null<TabbedSelector*> _selector;
rpl::event_stream<bool> _pauseAnimations;
int _contentMaxHeight = 0;
int _contentHeight = 0;
int _top = 0;
int _bottom = 0;
int _right = 0;
float64 _heightRatio = 1.;
int _minContentHeight = 0;
int _maxContentHeight = 0;
std::unique_ptr<Ui::PanelAnimation> _showAnimation;
Ui::Animations::Simple _a_show;
bool _shouldFinishHide = false;
bool _dropDown = false;
bool _hiding = false;
bool _hideAfterSlide = false;
QPixmap _cache;
Ui::Animations::Simple _a_opacity;
base::Timer _hideTimer;
};
} // namespace ChatHelpers

View File

@@ -0,0 +1,81 @@
/*
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/tabbed_section.h"
#include "chat_helpers/tabbed_selector.h"
#include "ui/ui_utility.h"
#include "window/window_session_controller.h"
#include "styles/style_chat_helpers.h"
namespace ChatHelpers {
object_ptr<Window::SectionWidget> TabbedMemento::createWidget(
QWidget *parent,
not_null<Window::SessionController*> controller,
Window::Column column,
const QRect &geometry) {
auto result = object_ptr<TabbedSection>(parent, controller);
result->setGeometry(geometry);
return result;
}
TabbedSection::TabbedSection(
QWidget *parent,
not_null<Window::SessionController*> controller)
: Window::SectionWidget(parent, controller)
, _selector(controller->tabbedSelector()) {
if (Ui::InFocusChain(_selector)) {
parent->window()->setFocus();
}
_selector->setParent(this);
_selector->setRoundRadius(0);
_selector->setGeometry(rect());
_selector->showStarted();
_selector->show();
_selector->setAfterShownCallback(nullptr);
_selector->setBeforeHidingCallback(nullptr);
setAttribute(Qt::WA_OpaquePaintEvent, true);
}
void TabbedSection::beforeHiding() {
_selector->beforeHiding();
}
void TabbedSection::afterShown() {
_selector->afterShown();
}
void TabbedSection::resizeEvent(QResizeEvent *e) {
_selector->setGeometry(rect());
}
void TabbedSection::showFinishedHook() {
afterShown();
}
bool TabbedSection::showInternal(
not_null<Window::SectionMemento*> memento,
const Window::SectionShow &params) {
return false;
}
bool TabbedSection::floatPlayerHandleWheelEvent(QEvent *e) {
return _selector->floatPlayerHandleWheelEvent(e);
}
QRect TabbedSection::floatPlayerAvailableRect() {
return _selector->floatPlayerAvailableRect();
}
TabbedSection::~TabbedSection() {
beforeHiding();
controller()->takeTabbedSelectorOwnershipFrom(this);
}
} // namespace ChatHelpers

View File

@@ -0,0 +1,62 @@
/*
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 "window/section_widget.h"
#include "window/section_memento.h"
namespace ChatHelpers {
class TabbedSelector;
class TabbedMemento : public Window::SectionMemento {
public:
TabbedMemento() = default;
TabbedMemento(TabbedMemento &&other) = default;
TabbedMemento &operator=(TabbedMemento &&other) = default;
object_ptr<Window::SectionWidget> createWidget(
QWidget *parent,
not_null<Window::SessionController*> controller,
Window::Column column,
const QRect &geometry) override;
};
class TabbedSection : public Window::SectionWidget {
public:
TabbedSection(
QWidget *parent,
not_null<Window::SessionController*> controller);
void beforeHiding();
void afterShown();
bool showInternal(
not_null<Window::SectionMemento*> memento,
const Window::SectionShow &params) override;
bool forceAnimateBack() const override {
return true;
}
// Float player interface.
bool floatPlayerHandleWheelEvent(QEvent *e) override;
QRect floatPlayerAvailableRect() override;
~TabbedSection();
protected:
void resizeEvent(QResizeEvent *e) override;
void showFinishedHook() override;
private:
const not_null<TabbedSelector*> _selector;
};
} // namespace ChatHelpers

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,463 @@
/*
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 "api/api_common.h"
#include "chat_helpers/compose/compose_features.h"
#include "ui/rp_widget.h"
#include "ui/controls/swipe_handler_data.h"
#include "ui/effects/animations.h"
#include "ui/effects/message_sending_animation_common.h"
#include "ui/effects/panel_animation.h"
#include "ui/cached_round_corners.h"
#include "mtproto/sender.h"
#include "base/object_ptr.h"
namespace InlineBots {
struct ResultSelected;
} // namespace InlineBots
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class PlainShadow;
class PopupMenu;
class ScrollArea;
class SettingsSlider;
class FlatLabel;
class BoxContent;
class TabbedSearch;
} // namespace Ui
namespace SendMenu {
struct Details;
} // namespace SendMenu
namespace style {
struct EmojiPan;
} // namespace style
namespace ChatHelpers {
class Show;
class EmojiListWidget;
class StickersListWidget;
class GifsListWidget;
enum class PauseReason;
enum class SelectorTab {
Emoji,
Stickers,
Gifs,
Masks,
};
struct FileChosen {
not_null<DocumentData*> document;
Api::SendOptions options;
Ui::MessageSendingAnimationFrom messageSendingFrom;
std::shared_ptr<Data::EmojiStatusCollectible> collectible;
TextWithTags caption;
};
struct PhotoChosen {
not_null<PhotoData*> photo;
Api::SendOptions options;
};
struct EmojiChosen {
EmojiPtr emoji;
Ui::MessageSendingAnimationFrom messageSendingFrom;
};
using InlineChosen = InlineBots::ResultSelected;
enum class TabbedSelectorMode {
Full,
EmojiOnly,
StickersOnly,
MediaEditor,
EmojiStatus,
ChannelStatus,
BackgroundEmoji,
FullReactions,
RecentReactions,
PeerTitle,
ChatIntro,
};
struct TabbedSelectorDescriptor {
std::shared_ptr<Show> show;
const style::EmojiPan &st;
PauseReason level = {};
TabbedSelectorMode mode = TabbedSelectorMode::Full;
Fn<QColor()> customTextColor;
ComposeFeatures features;
};
enum class TabbedSearchType {
Emoji,
Status,
ProfilePhoto,
Stickers,
Greeting,
};
[[nodiscard]] std::unique_ptr<Ui::TabbedSearch> MakeSearch(
not_null<Ui::RpWidget*> parent,
const style::EmojiPan &st,
Fn<void(std::vector<QString>&&)> callback,
not_null<Main::Session*> session,
TabbedSearchType type);
class TabbedSelector : public Ui::RpWidget {
public:
static constexpr auto kPickCustomTimeId = -1;
using Mode = TabbedSelectorMode;
enum class Action {
Update,
Cancel,
};
TabbedSelector(
QWidget *parent,
std::shared_ptr<Show> show,
PauseReason level,
Mode mode = Mode::Full);
TabbedSelector(
QWidget *parent,
TabbedSelectorDescriptor &&descriptor);
~TabbedSelector();
[[nodiscard]] const style::EmojiPan &st() const;
[[nodiscard]] Main::Session &session() const;
[[nodiscard]] PauseReason level() const;
[[nodiscard]] rpl::producer<EmojiChosen> emojiChosen() const;
[[nodiscard]] rpl::producer<FileChosen> customEmojiChosen() const;
[[nodiscard]] rpl::producer<FileChosen> fileChosen() const;
[[nodiscard]] rpl::producer<PhotoChosen> photoChosen() const;
[[nodiscard]] rpl::producer<InlineChosen> inlineResultChosen() const;
[[nodiscard]] rpl::producer<> cancelled() const;
[[nodiscard]] rpl::producer<> checkForHide() const;
[[nodiscard]] rpl::producer<> slideFinished() const;
[[nodiscard]] rpl::producer<> contextMenuRequested() const;
[[nodiscard]] rpl::producer<Action> choosingStickerUpdated() const;
void setAllowEmojiWithoutPremium(bool allow);
void setRoundRadius(int radius);
void refreshStickers();
void setCurrentPeer(PeerData *peer);
void provideRecentEmoji(
const std::vector<EmojiStatusId> &customRecentList);
void hideFinished();
void showStarted();
void beforeHiding();
void afterShown();
[[nodiscard]] int marginTop() const;
[[nodiscard]] int marginBottom() const;
[[nodiscard]] int scrollTop() const;
[[nodiscard]] int scrollBottom() const;
bool preventAutoHide() const;
bool isSliding() const {
return _a_slide.animating();
}
bool hasMenu() const;
void setAfterShownCallback(Fn<void(SelectorTab)> callback) {
_afterShownCallback = std::move(callback);
}
void setBeforeHidingCallback(Fn<void(SelectorTab)> callback) {
_beforeHidingCallback = std::move(callback);
}
void showMenuWithDetails(SendMenu::Details details);
void setDropDown(bool dropDown);
// Float player interface.
bool floatPlayerHandleWheelEvent(QEvent *e);
QRect floatPlayerAvailableRect() const;
auto showRequests() const {
return _showRequests.events();
}
class Inner;
class InnerFooter;
protected:
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
private:
class Tab {
public:
Tab(SelectorTab type, int index, object_ptr<Inner> widget);
object_ptr<Inner> takeWidget();
void returnWidget(object_ptr<Inner> widget);
[[nodiscard]] SelectorTab type() const {
return _type;
}
[[nodiscard]] int index() const {
return _index;
}
[[nodiscard]] Inner *widget() const {
return _weak;
}
[[nodiscard]] bool hasFooter() const {
return _footer != nullptr;
}
[[nodiscard]] not_null<InnerFooter*> footer() const {
return _footer;
}
void saveScrollTop();
void saveScrollTop(int scrollTop) {
_scrollTop = scrollTop;
}
[[nodiscard]] int getScrollTop() const {
return _scrollTop;
}
private:
const SelectorTab _type;
const int _index;
object_ptr<Inner> _widget = { nullptr };
QPointer<Inner> _weak;
object_ptr<InnerFooter> _footer;
int _scrollTop = 0;
};
bool full() const;
bool mediaEditor() const;
bool tabbed() const;
bool hasEmojiTab() const;
bool hasStickersTab() const;
bool hasGifsTab() const;
bool hasMasksTab() const;
Tab createTab(SelectorTab type, int index);
void paintSlideFrame(QPainter &p);
void paintBgRoundedPart(QPainter &p);
void paintContent(QPainter &p);
void checkRestrictedPeer();
bool isRestrictedView();
void updateRestrictedLabelGeometry();
void updateScrollGeometry(QSize oldSize);
void updateFooterGeometry();
void handleScroll();
QImage grabForAnimation();
void scrollToY(int y);
void showAll();
void hideForSliding();
SelectorTab typeByIndex(int index) const;
int indexByType(SelectorTab type) const;
bool hasSectionIcons() const;
void setWidgetToScrollArea();
void createTabsSlider();
void fillTabsSliderSections();
void updateTabsSliderGeometry();
void switchTab();
not_null<Tab*> getTab(int index);
not_null<const Tab*> getTab(int index) const;
not_null<Tab*> currentTab();
not_null<const Tab*> currentTab() const;
not_null<EmojiListWidget*> emoji() const;
not_null<StickersListWidget*> stickers() const;
not_null<GifsListWidget*> gifs() const;
not_null<StickersListWidget*> masks() const;
void reinstallSwipe(not_null<Ui::RpWidget*> widget);
const style::EmojiPan &_st;
const ComposeFeatures _features;
const std::shared_ptr<Show> _show;
const PauseReason _level = {};
const Fn<QColor()> _customTextColor;
Ui::Controls::SwipeBackResult _swipeBackData;
Mode _mode = Mode::Full;
int _roundRadius = 0;
int _footerTop = 0;
bool _noFooter = false;
Ui::CornersPixmaps _panelRounding;
Ui::CornersPixmaps _categoriesRounding;
PeerData *_currentPeer = nullptr;
class SlideAnimation;
std::unique_ptr<SlideAnimation> _slideAnimation;
Ui::Animations::Simple _a_slide;
object_ptr<Ui::SettingsSlider> _tabsSlider = { nullptr };
object_ptr<Ui::PlainShadow> _topShadow;
object_ptr<Ui::PlainShadow> _bottomShadow;
object_ptr<Ui::ScrollArea> _scroll;
object_ptr<Ui::FlatLabel> _restrictedLabel = { nullptr };
QString _restrictedLabelKey;
std::vector<Tab> _tabs;
SelectorTab _currentTabType = SelectorTab::Emoji;
const bool _hasEmojiTab;
const bool _hasStickersTab;
const bool _hasGifsTab;
const bool _hasMasksTab;
const bool _tabbed;
bool _dropDown = false;
base::unique_qptr<Ui::PopupMenu> _menu;
Fn<void(SelectorTab)> _afterShownCallback;
Fn<void(SelectorTab)> _beforeHidingCallback;
rpl::event_stream<> _showRequests;
rpl::event_stream<> _slideFinished;
rpl::lifetime _swipeLifetime;
};
class TabbedSelector::Inner : public Ui::RpWidget {
public:
Inner(
QWidget *parent,
std::shared_ptr<Show> show,
PauseReason level);
Inner(
QWidget *parent,
const style::EmojiPan &st,
std::shared_ptr<Show> show,
Fn<bool()> paused);
[[nodiscard]] Main::Session &session() const {
return *_session;
}
[[nodiscard]] const style::EmojiPan &st() const {
return _st;
}
[[nodiscard]] Fn<bool()> pausedMethod() const {
return _paused;
}
[[nodiscard]] bool paused() const {
return _paused();
}
[[nodiscard]] int getVisibleTop() const {
return _visibleTop;
}
[[nodiscard]] int getVisibleBottom() const {
return _visibleBottom;
}
void setMinimalHeight(int newWidth, int newMinimalHeight);
[[nodiscard]] rpl::producer<> checkForHide() const {
return _checkForHide.events();
}
[[nodiscard]] bool preventAutoHide() const {
return _preventHideWithBox;
}
virtual void refreshRecent() = 0;
virtual void preloadImages() {
}
void hideFinished();
void panelHideFinished();
virtual void clearSelection() = 0;
virtual void afterShown() {
}
virtual void beforeHiding() {
}
[[nodiscard]] virtual base::unique_qptr<Ui::PopupMenu> fillContextMenu(
const SendMenu::Details &details) {
return nullptr;
}
rpl::producer<int> scrollToRequests() const;
rpl::producer<bool> disableScrollRequests() const;
virtual object_ptr<InnerFooter> createFooter() = 0;
protected:
void visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) override;
int minimalHeight() const;
virtual int defaultMinimalHeight() const;
int resizeGetHeight(int newWidth) override final;
virtual int countDesiredHeight(int newWidth) = 0;
virtual InnerFooter *getFooter() const = 0;
virtual void processHideFinished() {
}
virtual void processPanelHideFinished() {
}
void scrollTo(int y);
void disableScroll(bool disabled);
void checkHideWithBox(object_ptr<Ui::BoxContent> box);
void paintEmptySearchResults(
Painter &p,
const style::icon &icon,
const QString &text) const;
private:
const style::EmojiPan &_st;
const std::shared_ptr<Show> _show;
const not_null<Main::Session*> _session;
const Fn<bool()> _paused;
int _visibleTop = 0;
int _visibleBottom = 0;
std::optional<int> _minimalHeight;
rpl::event_stream<int> _scrollToRequests;
rpl::event_stream<bool> _disableScrollRequests;
rpl::event_stream<> _checkForHide;
bool _preventHideWithBox = false;
};
class TabbedSelector::InnerFooter : public Ui::RpWidget {
public:
InnerFooter(QWidget *parent, const style::EmojiPan &st);
[[nodiscard]] const style::EmojiPan &st() const;
protected:
virtual void processHideFinished() {
}
virtual void processPanelHideFinished() {
}
friend class Inner;
private:
const style::EmojiPan &_st;
};
} // namespace ChatHelpers

View File

@@ -0,0 +1,394 @@
/*
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/ttl_media_layer_widget.h"
#include "base/event_filter.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "editor/editor_layer_widget.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/view/history_view_element.h"
#include "history/view/media/history_view_document.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "mainwidget.h"
#include "media/audio/media_audio.h"
#include "media/player/media_player_instance.h"
#include "ui/chat/chat_style.h"
#include "ui/chat/chat_theme.h"
#include "ui/effects/path_shift_gradient.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/tooltip.h"
#include "ui/ui_utility.h"
#include "window/section_widget.h" // Window::ChatThemeValueFromPeer.
#include "window/themes/window_theme.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_dialogs.h"
namespace ChatHelpers {
namespace {
class PreviewDelegate final : public HistoryView::DefaultElementDelegate {
public:
PreviewDelegate(
not_null<QWidget*> parent,
not_null<Ui::ChatStyle*> st,
rpl::producer<bool> chatWideValue,
Fn<void()> update);
bool elementAnimationsPaused() override;
not_null<Ui::PathShiftGradient*> elementPathShiftGradient() override;
HistoryView::Context elementContext() override;
HistoryView::ElementChatMode elementChatMode() override;
private:
const not_null<QWidget*> _parent;
const std::unique_ptr<Ui::PathShiftGradient> _pathGradient;
rpl::variable<bool> _chatWide;
};
PreviewDelegate::PreviewDelegate(
not_null<QWidget*> parent,
not_null<Ui::ChatStyle*> st,
rpl::producer<bool> chatWideValue,
Fn<void()> update)
: _parent(parent)
, _pathGradient(HistoryView::MakePathShiftGradient(st, update))
, _chatWide(std::move(chatWideValue)) {
}
bool PreviewDelegate::elementAnimationsPaused() {
return _parent->window()->isActiveWindow();
}
not_null<Ui::PathShiftGradient*> PreviewDelegate::elementPathShiftGradient() {
return _pathGradient.get();
}
HistoryView::Context PreviewDelegate::elementContext() {
return HistoryView::Context::TTLViewer;
}
HistoryView::ElementChatMode PreviewDelegate::elementChatMode() {
using Mode = HistoryView::ElementChatMode;
return _chatWide.current() ? Mode::Wide : Mode::Default;
}
class PreviewWrap final : public Ui::RpWidget {
public:
PreviewWrap(
not_null<Ui::RpWidget*> parent,
not_null<HistoryItem*> item,
rpl::producer<QRect> viewportValue,
rpl::producer<bool> chatWideValue,
rpl::producer<std::shared_ptr<Ui::ChatTheme>> theme);
~PreviewWrap();
[[nodiscard]] rpl::producer<> closeRequests() const;
private:
void paintEvent(QPaintEvent *e) override;
void createView();
[[nodiscard]] bool goodItem() const;
void clear();
const not_null<HistoryItem*> _item;
const std::unique_ptr<Ui::ChatStyle> _style;
const std::unique_ptr<PreviewDelegate> _delegate;
rpl::variable<QRect> _globalViewport;
rpl::variable<bool> _chatWide;
std::shared_ptr<Ui::ChatTheme> _theme;
std::unique_ptr<HistoryView::Element> _element;
QRect _viewport;
QRect _elementGeometry;
rpl::variable<QRect> _elementInner;
rpl::lifetime _elementLifetime;
QImage _lastFrameCache;
rpl::event_stream<> _closeRequests;
};
PreviewWrap::PreviewWrap(
not_null<Ui::RpWidget*> parent,
not_null<HistoryItem*> item,
rpl::producer<QRect> viewportValue,
rpl::producer<bool> chatWideValue,
rpl::producer<std::shared_ptr<Ui::ChatTheme>> theme)
: RpWidget(parent)
, _item(item)
, _style(std::make_unique<Ui::ChatStyle>(
item->history()->session().colorIndicesValue()))
, _delegate(std::make_unique<PreviewDelegate>(
parent,
_style.get(),
std::move(chatWideValue),
[=] { update(_elementGeometry); }))
, _globalViewport(std::move(viewportValue)) {
const auto closeCallback = [=] { _closeRequests.fire({}); };
HistoryView::TTLVoiceStops(
item->fullId()
) | rpl::on_next([=] {
_lastFrameCache = Ui::GrabWidgetToImage(this, _elementGeometry);
closeCallback();
}, lifetime());
const auto isRound = _item
&& _item->media()
&& _item->media()->document()
&& _item->media()->document()->isVideoMessage();
std::move(
theme
) | rpl::on_next([=](std::shared_ptr<Ui::ChatTheme> theme) {
_theme = std::move(theme);
_style->apply(_theme.get());
}, lifetime());
const auto session = &_item->history()->session();
session->data().viewRepaintRequest(
) | rpl::on_next([=](not_null<const HistoryView::Element*> view) {
if (view == _element.get()) {
update(_elementGeometry);
}
}, lifetime());
session->data().itemViewRefreshRequest(
) | rpl::on_next([=](not_null<const HistoryItem*> item) {
if (item == _item) {
if (goodItem()) {
createView();
update();
} else {
clear();
_closeRequests.fire({});
}
}
}, lifetime());
session->data().itemDataChanges(
) | rpl::on_next([=](not_null<HistoryItem*> item) {
if (item == _item) {
_element->itemDataChanged();
}
}, lifetime());
session->data().itemRemoved(
) | rpl::on_next([=](not_null<const HistoryItem*> item) {
if (item == _item) {
_closeRequests.fire({});
}
}, lifetime());
{
const auto close = Ui::CreateChild<Ui::RoundButton>(
this,
item->out()
? tr::lng_close()
: tr::lng_ttl_voice_close_in(),
st::ttlMediaButton);
close->setFullRadius(true);
close->setClickedCallback(closeCallback);
close->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
rpl::combine(
sizeValue(),
_elementInner.value()
) | rpl::on_next([=](QSize size, QRect inner) {
close->moveToLeft(
inner.x() + (inner.width() - close->width()) / 2,
(size.height()
- close->height()
- st::ttlMediaButtonBottomSkip));
}, close->lifetime());
}
QWidget::setAttribute(Qt::WA_OpaquePaintEvent, false);
createView();
{
auto text = item->out()
? (isRound
? tr::lng_ttl_round_tooltip_out
: tr::lng_ttl_voice_tooltip_out)(
lt_user,
rpl::single(
item->history()->peer->shortName()
) | rpl::map(tr::rich),
tr::rich)
: (isRound
? tr::lng_ttl_round_tooltip_in
: tr::lng_ttl_voice_tooltip_in)(tr::rich);
const auto tooltip = Ui::CreateChild<Ui::ImportantTooltip>(
this,
object_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(
this,
Ui::MakeNiceTooltipLabel(
parent,
std::move(text),
st::dialogsStoriesTooltipMaxWidth,
st::ttlMediaImportantTooltipLabel),
st::defaultImportantTooltip.padding),
st::dialogsStoriesTooltip);
tooltip->toggleFast(true);
_elementInner.value(
) | rpl::filter([](const QRect &inner) {
return !inner.isEmpty();
}) | rpl::on_next([=](const QRect &inner) {
tooltip->pointAt(inner, RectPart::Top, [=](QSize size) {
return QPoint{
inner.x() + (inner.width() - size.width()) / 2,
(inner.y()
- st::normalFont->height
- size.height()
- st::defaultImportantTooltip.padding.top()),
};
});
}, tooltip->lifetime());
}
}
rpl::producer<> PreviewWrap::closeRequests() const {
return _closeRequests.events();
}
bool PreviewWrap::goodItem() const {
const auto media = _item->media();
if (!media || !media->ttlSeconds()) {
return false;
}
const auto document = media->document();
return document
&& (document->isVoiceMessage() || document->isVideoMessage());
}
void PreviewWrap::createView() {
clear();
_element = _item->createView(_delegate.get());
_element->initDimensions();
rpl::combine(
sizeValue(),
_globalViewport.value()
) | rpl::on_next([=](QSize outer, QRect globalViewport) {
_viewport = globalViewport.isEmpty()
? rect()
: mapFromGlobal(globalViewport);
if (_viewport.width() < st::msgMinWidth) {
return;
}
_element->resizeGetHeight(_viewport.width());
_elementGeometry = QRect(
(_viewport.width() - _element->width()) / 2,
(_viewport.height() - _element->height()) / 2,
_element->width(),
_element->height()
).translated(_viewport.topLeft());
_elementInner = _element->innerGeometry().translated(
_elementGeometry.topLeft());
update();
}, _elementLifetime);
}
void PreviewWrap::clear() {
_elementLifetime.destroy();
_element = nullptr;
}
PreviewWrap::~PreviewWrap() {
clear();
}
void PreviewWrap::paintEvent(QPaintEvent *e) {
if (!_element || _elementGeometry.isEmpty()) {
return;
}
auto p = Painter(this);
p.translate(_elementGeometry.topLeft());
if (!_lastFrameCache.isNull()) {
p.drawImage(0, 0, _lastFrameCache);
} else {
auto context = _theme->preparePaintContext(
_style.get(),
Rect(_element->currentSize()),
Rect(_element->currentSize()),
!window()->isActiveWindow());
context.outbg = _element->hasOutLayout();
_element->draw(p, context);
}
}
rpl::producer<QRect> GlobalViewportForWindow(
not_null<Window::SessionController*> controller) {
const auto delegate = controller->window().floatPlayerDelegate();
return rpl::single(rpl::empty) | rpl::then(
delegate->floatPlayerAreaUpdates()
) | rpl::map([=] {
auto section = (Media::Player::FloatSectionDelegate*)nullptr;
delegate->floatPlayerEnumerateSections([&](
not_null<Media::Player::FloatSectionDelegate*> check,
Window::Column column) {
if ((column == Window::Column::First && !section)
|| column == Window::Column::Second) {
section = check;
}
});
if (section) {
const auto rect = section->floatPlayerAvailableRect();
if (rect.width() >= st::msgMinWidth) {
return rect;
}
}
return QRect();
});
}
} // namespace
void ShowTTLMediaLayerWidget(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item) {
const auto parent = controller->content();
const auto show = controller->uiShow();
auto preview = base::make_unique_q<PreviewWrap>(
parent,
item,
GlobalViewportForWindow(controller),
controller->adaptive().chatWideValue(),
Window::ChatThemeValueFromPeer(
controller,
item->history()->peer));
preview->closeRequests(
) | rpl::on_next([=] {
show->hideLayer();
}, preview->lifetime());
auto layer = std::make_unique<Editor::LayerWidget>(
parent,
std::move(preview));
layer->lifetime().add([] { ::Media::Player::instance()->stop(); });
base::install_event_filter(layer.get(), [=](not_null<QEvent*> e) {
if (e->type() == QEvent::KeyPress) {
const auto k = static_cast<QKeyEvent*>(e.get());
if (k->key() == Qt::Key_Escape) {
show->hideLayer();
}
return base::EventFilterResult::Cancel;
}
return base::EventFilterResult::Continue;
});
controller->showLayer(std::move(layer), Ui::LayerOption::KeepOther);
}
} // namespace ChatHelpers

View File

@@ -0,0 +1,22 @@
/*
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 HistoryItem;
namespace Window {
class SessionController;
} // namespace Window
namespace ChatHelpers {
void ShowTTLMediaLayerWidget(
not_null<Window::SessionController*> controller,
not_null<HistoryItem*> item);
} // namespace ChatHelpers