init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
This commit is contained in:
49
Telegram/SourceFiles/chat_helpers/bot_command.cpp
Normal file
49
Telegram/SourceFiles/chat_helpers/bot_command.cpp
Normal 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
|
||||
31
Telegram/SourceFiles/chat_helpers/bot_command.h
Normal file
31
Telegram/SourceFiles/chat_helpers/bot_command.h
Normal 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
|
||||
392
Telegram/SourceFiles/chat_helpers/bot_keyboard.cpp
Normal file
392
Telegram/SourceFiles/chat_helpers/bot_keyboard.cpp
Normal 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;
|
||||
104
Telegram/SourceFiles/chat_helpers/bot_keyboard.h
Normal file
104
Telegram/SourceFiles/chat_helpers/bot_keyboard.h
Normal 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;
|
||||
|
||||
};
|
||||
1747
Telegram/SourceFiles/chat_helpers/chat_helpers.style
Normal file
1747
Telegram/SourceFiles/chat_helpers/chat_helpers.style
Normal file
File diff suppressed because it is too large
Load Diff
36
Telegram/SourceFiles/chat_helpers/compose/compose_features.h
Normal file
36
Telegram/SourceFiles/chat_helpers/compose/compose_features.h
Normal 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
|
||||
52
Telegram/SourceFiles/chat_helpers/compose/compose_show.cpp
Normal file
52
Telegram/SourceFiles/chat_helpers/compose/compose_show.cpp
Normal 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
|
||||
69
Telegram/SourceFiles/chat_helpers/compose/compose_show.h
Normal file
69
Telegram/SourceFiles/chat_helpers/compose/compose_show.h
Normal 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
|
||||
493
Telegram/SourceFiles/chat_helpers/emoji_interactions.cpp
Normal file
493
Telegram/SourceFiles/chat_helpers/emoji_interactions.cpp
Normal 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
|
||||
142
Telegram/SourceFiles/chat_helpers/emoji_interactions.h
Normal file
142
Telegram/SourceFiles/chat_helpers/emoji_interactions.h
Normal 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
|
||||
759
Telegram/SourceFiles/chat_helpers/emoji_keywords.cpp
Normal file
759
Telegram/SourceFiles/chat_helpers/emoji_keywords.cpp
Normal 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
|
||||
87
Telegram/SourceFiles/chat_helpers/emoji_keywords.h
Normal file
87
Telegram/SourceFiles/chat_helpers/emoji_keywords.h
Normal 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
|
||||
2956
Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp
Normal file
2956
Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp
Normal file
File diff suppressed because it is too large
Load Diff
500
Telegram/SourceFiles/chat_helpers/emoji_list_widget.h
Normal file
500
Telegram/SourceFiles/chat_helpers/emoji_list_widget.h
Normal 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
|
||||
567
Telegram/SourceFiles/chat_helpers/emoji_sets_manager.cpp
Normal file
567
Telegram/SourceFiles/chat_helpers/emoji_sets_manager.cpp
Normal 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
|
||||
33
Telegram/SourceFiles/chat_helpers/emoji_sets_manager.h
Normal file
33
Telegram/SourceFiles/chat_helpers/emoji_sets_manager.h
Normal 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
|
||||
1144
Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp
Normal file
1144
Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.cpp
Normal file
File diff suppressed because it is too large
Load Diff
116
Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h
Normal file
116
Telegram/SourceFiles/chat_helpers/emoji_suggestions_widget.h
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "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
|
||||
1819
Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp
Normal file
1819
Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp
Normal file
File diff suppressed because it is too large
Load Diff
227
Telegram/SourceFiles/chat_helpers/field_autocomplete.h
Normal file
227
Telegram/SourceFiles/chat_helpers/field_autocomplete.h
Normal 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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
};
|
||||
1049
Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp
Normal file
1049
Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp
Normal file
File diff suppressed because it is too large
Load Diff
227
Telegram/SourceFiles/chat_helpers/gifs_list_widget.h
Normal file
227
Telegram/SourceFiles/chat_helpers/gifs_list_widget.h
Normal 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
|
||||
1506
Telegram/SourceFiles/chat_helpers/message_field.cpp
Normal file
1506
Telegram/SourceFiles/chat_helpers/message_field.cpp
Normal file
File diff suppressed because it is too large
Load Diff
218
Telegram/SourceFiles/chat_helpers/message_field.h
Normal file
218
Telegram/SourceFiles/chat_helpers/message_field.h
Normal 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);
|
||||
@@ -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
|
||||
@@ -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
|
||||
481
Telegram/SourceFiles/chat_helpers/spellchecker_common.cpp
Normal file
481
Telegram/SourceFiles/chat_helpers/spellchecker_common.cpp
Normal 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
|
||||
76
Telegram/SourceFiles/chat_helpers/spellchecker_common.h
Normal file
76
Telegram/SourceFiles/chat_helpers/spellchecker_common.h
Normal 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
|
||||
147
Telegram/SourceFiles/chat_helpers/stickers_dice_pack.cpp
Normal file
147
Telegram/SourceFiles/chat_helpers/stickers_dice_pack.cpp
Normal 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
|
||||
61
Telegram/SourceFiles/chat_helpers/stickers_dice_pack.h
Normal file
61
Telegram/SourceFiles/chat_helpers/stickers_dice_pack.h
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
532
Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp
Normal file
532
Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.cpp
Normal 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
|
||||
165
Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.h
Normal file
165
Telegram/SourceFiles/chat_helpers/stickers_emoji_pack.h
Normal 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
|
||||
153
Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.cpp
Normal file
153
Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.cpp
Normal 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
|
||||
66
Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.h
Normal file
66
Telegram/SourceFiles/chat_helpers/stickers_gift_box_pack.h
Normal 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
|
||||
1554
Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp
Normal file
1554
Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp
Normal file
File diff suppressed because it is too large
Load Diff
343
Telegram/SourceFiles/chat_helpers/stickers_list_footer.h
Normal file
343
Telegram/SourceFiles/chat_helpers/stickers_list_footer.h
Normal 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
|
||||
2981
Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp
Normal file
2981
Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp
Normal file
File diff suppressed because it is too large
Load Diff
462
Telegram/SourceFiles/chat_helpers/stickers_list_widget.h
Normal file
462
Telegram/SourceFiles/chat_helpers/stickers_list_widget.h
Normal 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
|
||||
365
Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp
Normal file
365
Telegram/SourceFiles/chat_helpers/stickers_lottie.cpp
Normal 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
|
||||
142
Telegram/SourceFiles/chat_helpers/stickers_lottie.h
Normal file
142
Telegram/SourceFiles/chat_helpers/stickers_lottie.h
Normal 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
|
||||
520
Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp
Normal file
520
Telegram/SourceFiles/chat_helpers/tabbed_panel.cpp
Normal 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
|
||||
133
Telegram/SourceFiles/chat_helpers/tabbed_panel.h
Normal file
133
Telegram/SourceFiles/chat_helpers/tabbed_panel.h
Normal 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
|
||||
81
Telegram/SourceFiles/chat_helpers/tabbed_section.cpp
Normal file
81
Telegram/SourceFiles/chat_helpers/tabbed_section.cpp
Normal 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 ¶ms) {
|
||||
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
|
||||
62
Telegram/SourceFiles/chat_helpers/tabbed_section.h
Normal file
62
Telegram/SourceFiles/chat_helpers/tabbed_section.h
Normal 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 ¶ms) 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
|
||||
1580
Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp
Normal file
1580
Telegram/SourceFiles/chat_helpers/tabbed_selector.cpp
Normal file
File diff suppressed because it is too large
Load Diff
463
Telegram/SourceFiles/chat_helpers/tabbed_selector.h
Normal file
463
Telegram/SourceFiles/chat_helpers/tabbed_selector.h
Normal 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
|
||||
394
Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.cpp
Normal file
394
Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.cpp
Normal 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
|
||||
22
Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.h
Normal file
22
Telegram/SourceFiles/chat_helpers/ttl_media_layer_widget.h
Normal 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
|
||||
Reference in New Issue
Block a user