Files
tdesktop/Telegram/SourceFiles/calls/group/calls_group_message_field.cpp
allhaileris afb81b8278
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
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
init
2026-02-16 15:50:16 +03:00

663 lines
17 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/group/calls_group_message_field.h"
#include "base/event_filter.h"
#include "boxes/premium_preview_box.h"
#include "calls/group/calls_group_messages.h"
#include "chat_helpers/compose/compose_show.h"
#include "chat_helpers/emoji_suggestions_widget.h"
#include "chat_helpers/message_field.h"
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "core/ui_integration.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/stickers/data_stickers.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "history/view/reactions/history_view_reactions_selector.h"
#include "history/view/reactions/history_view_reactions_strip.h"
#include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "ui/controls/emoji_button.h"
#include "ui/controls/send_button.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/scroll_area.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "ui/userpic_view.h"
#include "styles/style_calls.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_chat.h"
#include "styles/style_media_view.h"
namespace Calls::Group {
namespace {
constexpr auto kErrorLimit = 99;
using Chosen = HistoryView::Reactions::ChosenReaction;
} // namespace
class ReactionPanel final {
public:
ReactionPanel(
not_null<QWidget*> outer,
std::shared_ptr<ChatHelpers::Show> show,
rpl::producer<QRect> fieldGeometry);
~ReactionPanel();
[[nodiscard]] rpl::producer<Chosen> chosen() const;
void show();
void hide();
void raise();
void hideIfCollapsed();
void collapse();
private:
struct Hiding;
void create();
void updateShowState();
void fadeOutSelector();
void startAnimation();
const not_null<QWidget*> _outer;
const std::shared_ptr<ChatHelpers::Show> _show;
std::unique_ptr<Ui::RpWidget> _parent;
std::unique_ptr<HistoryView::Reactions::Selector> _selector;
std::vector<std::unique_ptr<Hiding>> _hiding;
rpl::event_stream<Chosen> _chosen;
Ui::Animations::Simple _showing;
rpl::variable<float64> _shownValue;
rpl::variable<QRect> _fieldGeometry;
rpl::variable<bool> _expanded;
rpl::variable<bool> _shown = false;
};
struct ReactionPanel::Hiding {
explicit Hiding(not_null<QWidget*> parent) : widget(parent) {
}
Ui::RpWidget widget;
Ui::Animations::Simple animation;
QImage frame;
};
ReactionPanel::ReactionPanel(
not_null<QWidget*> outer,
std::shared_ptr<ChatHelpers::Show> show,
rpl::producer<QRect> fieldGeometry)
: _outer(outer)
, _show(std::move(show))
, _fieldGeometry(std::move(fieldGeometry)) {
}
ReactionPanel::~ReactionPanel() = default;
auto ReactionPanel::chosen() const -> rpl::producer<Chosen> {
return _chosen.events();
}
void ReactionPanel::show() {
if (_shown.current()) {
return;
}
create();
if (!_selector) {
return;
}
const auto duration = st::defaultPanelAnimation.heightDuration
* st::defaultPopupMenu.showDuration;
_shown = true;
_showing.start([=] { updateShowState(); }, 0., 1., duration);
updateShowState();
_parent->show();
}
void ReactionPanel::hide() {
if (!_selector) {
return;
}
_selector->beforeDestroy();
if (!anim::Disabled()) {
fadeOutSelector();
}
_shown = false;
_expanded = false;
_showing.stop();
_selector = nullptr;
_parent = nullptr;
}
void ReactionPanel::raise() {
if (_parent) {
_parent->raise();
}
}
void ReactionPanel::hideIfCollapsed() {
if (!_expanded.current()) {
hide();
}
}
void ReactionPanel::collapse() {
if (_expanded.current()) {
hide();
show();
}
}
void ReactionPanel::create() {
auto reactions = Data::LookupPossibleReactions(&_show->session());
if (reactions.recent.empty()) {
return;
}
_parent = std::make_unique<Ui::RpWidget>(_outer);
_parent->show();
_parent->events() | rpl::on_next([=](not_null<QEvent*> e) {
if (e->type() == QEvent::MouseButtonPress) {
const auto event = static_cast<QMouseEvent*>(e.get());
if (event->button() == Qt::LeftButton) {
if (!_selector
|| !_selector->geometry().contains(event->pos())) {
collapse();
}
}
}
}, _parent->lifetime());
_selector = std::make_unique<HistoryView::Reactions::Selector>(
_parent.get(),
st::storiesReactionsPan,
_show,
std::move(reactions),
TextWithEntities(),
[=](bool fast) { hide(); },
nullptr, // iconFactory
nullptr, // paused
true);
_selector->chosen(
) | rpl::on_next([=](Chosen reaction) {
if (reaction.id.custom() && !_show->session().premium()) {
ShowPremiumPreviewBox(
_show,
PremiumFeature::AnimatedEmoji);
} else {
_chosen.fire(std::move(reaction));
hide();
}
}, _selector->lifetime());
const auto desiredWidth = st::storiesReactionsWidth;
const auto maxWidth = desiredWidth * 2;
const auto width = _selector->countWidth(desiredWidth, maxWidth);
const auto margins = _selector->marginsForShadow();
const auto categoriesTop = _selector->extendTopForCategoriesAndAbout(
width);
const auto full = margins.left() + width + margins.right();
_shownValue = 0.;
rpl::combine(
_fieldGeometry.value(),
_shownValue.value(),
_expanded.value()
) | rpl::on_next([=](QRect field, float64 shown, bool expanded) {
const auto width = margins.left()
+ _selector->countAppearedWidth(shown)
+ margins.right();
const auto available = field.y();
const auto min = st::storiesReactionsBottomSkip
+ st::reactStripHeight;
const auto max = min
+ margins.top()
+ categoriesTop
+ st::storiesReactionsAddedTop;
const auto height = expanded ? std::min(available, max) : min;
const auto top = field.y() - height;
const auto shift = (width / 2);
const auto right = (field.x() + field.width() / 2 + shift);
_parent->setGeometry(QRect((right - width), top, full, height));
const auto innerTop = height
- st::storiesReactionsBottomSkip
- st::reactStripHeight;
const auto maxAdded = innerTop - margins.top() - categoriesTop;
const auto added = std::min(maxAdded, st::storiesReactionsAddedTop);
_selector->setSpecialExpandTopSkip(added);
_selector->initGeometry(innerTop);
}, _selector->lifetime());
_selector->willExpand(
) | rpl::on_next([=] {
_expanded = true;
const auto raw = _parent.get();
base::install_event_filter(raw, qApp, [=](not_null<QEvent*> e) {
if (e->type() == QEvent::MouseButtonPress) {
const auto event = static_cast<QMouseEvent*>(e.get());
if (event->button() == Qt::LeftButton) {
if (!_selector
|| !_selector->geometry().contains(
_parent->mapFromGlobal(event->globalPos()))) {
collapse();
}
}
}
return base::EventFilterResult::Continue;
});
}, _selector->lifetime());
_selector->escapes() | rpl::on_next([=] {
collapse();
}, _selector->lifetime());
}
void ReactionPanel::fadeOutSelector() {
const auto geometry = Ui::MapFrom(
_outer,
_parent.get(),
_selector->geometry());
_hiding.push_back(std::make_unique<Hiding>(_outer));
const auto raw = _hiding.back().get();
raw->frame = Ui::GrabWidgetToImage(_selector.get());
raw->widget.setGeometry(geometry);
raw->widget.show();
raw->widget.paintRequest(
) | rpl::on_next([=] {
if (const auto opacity = raw->animation.value(0.)) {
auto p = QPainter(&raw->widget);
p.setOpacity(opacity);
p.drawImage(0, 0, raw->frame);
}
}, raw->widget.lifetime());
Ui::PostponeCall(&raw->widget, [=] {
raw->animation.start([=] {
if (raw->animation.animating()) {
raw->widget.update();
} else {
const auto i = ranges::find(
_hiding,
raw,
&std::unique_ptr<Hiding>::get);
if (i != end(_hiding)) {
_hiding.erase(i);
}
}
}, 1., 0., st::slideWrapDuration);
});
}
void ReactionPanel::updateShowState() {
const auto progress = _showing.value(_shown.current() ? 1. : 0.);
const auto opacity = 1.;
const auto appearing = _showing.animating();
const auto toggling = false;
_shownValue = progress;
_selector->updateShowState(progress, opacity, appearing, toggling);
}
MessageField::MessageField(
not_null<QWidget*> parent,
std::shared_ptr<ChatHelpers::Show> show,
PeerData *peer)
: _parent(parent)
, _show(std::move(show))
, _wrap(std::make_unique<Ui::RpWidget>(_parent))
, _limit(_show->session().appConfig().groupCallMessageLengthLimit()) {
createControls(peer);
}
MessageField::~MessageField() = default;
void MessageField::createControls(PeerData *peer) {
setupBackground();
const auto &st = st::storiesComposeControls;
_field = Ui::CreateChild<Ui::InputField>(
_wrap.get(),
st.field,
Ui::InputField::Mode::MultiLine,
tr::lng_message_ph());
_field->setMaxLength(_limit + kErrorLimit);
_field->setMinHeight(
st::historySendSize.height() - 2 * st::historySendPadding);
_field->setMaxHeight(st::historyComposeFieldMaxHeight);
_field->setDocumentMargin(4.);
_field->setAdditionalMargin(style::ConvertScale(4) - 4);
_reactionPanel = std::make_unique<ReactionPanel>(
_parent,
_show,
_wrap->geometryValue());
_fieldFocused = _field->focusedChanges();
_fieldEmpty = _field->changes() | rpl::map([field = _field] {
return field->getLastText().trimmed().isEmpty();
});
rpl::combine(
_fieldFocused.value(),
_fieldEmpty.value()
) | rpl::on_next([=](bool focused, bool empty) {
if (!focused) {
_reactionPanel->hideIfCollapsed();
} else if (empty) {
_reactionPanel->show();
} else {
_reactionPanel->hide();
}
}, _field->lifetime());
_reactionPanel->chosen(
) | rpl::on_next([=](Chosen reaction) {
if (const auto customId = reaction.id.custom()) {
const auto document = _show->session().data().document(customId);
if (const auto sticker = document->sticker()) {
if (const auto alt = sticker->alt; !alt.isEmpty()) {
const auto length = int(alt.size());
const auto data = Data::SerializeCustomEmojiId(customId);
const auto tag = Ui::InputField::CustomEmojiLink(data);
_submitted.fire({ alt, { { 0, length, tag } } });
}
}
} else {
_submitted.fire({ reaction.id.emoji() });
}
_reactionPanel->hide();
}, _field->lifetime());
const auto show = _show;
const auto allow = [=](not_null<DocumentData*> emoji) {
if (peer && Data::AllowEmojiWithoutPremium(peer, emoji)) {
return true;
}
return false;
};
InitMessageFieldHandlers({
.session = &show->session(),
.show = show,
.field = _field,
.customEmojiPaused = [=] {
return show->paused(ChatHelpers::PauseReason::Layer);
},
.allowPremiumEmoji = allow,
.fieldStyle = &st.files.caption,
.allowMarkdownTags = {
Ui::InputField::kTagBold,
Ui::InputField::kTagItalic,
Ui::InputField::kTagUnderline,
Ui::InputField::kTagStrikeOut,
Ui::InputField::kTagSpoiler,
},
});
Ui::Emoji::SuggestionsController::Init(
_parent,
_field,
&_show->session(),
{
.suggestCustomEmoji = true,
.allowCustomWithoutPremium = allow,
.st = &st.suggestions,
});
_send = Ui::CreateChild<Ui::SendButton>(_wrap.get(), st.send);
_send->show();
using Selector = ChatHelpers::TabbedSelector;
_emojiPanel = std::make_unique<ChatHelpers::TabbedPanel>(
_parent,
ChatHelpers::TabbedPanelDescriptor{
.ownedSelector = object_ptr<Selector>(
nullptr,
ChatHelpers::TabbedSelectorDescriptor{
.show = _show,
.st = st.tabbed,
.level = ChatHelpers::PauseReason::Layer,
.mode = ChatHelpers::TabbedSelector::Mode::EmojiOnly,
.features = {
.stickersSettings = false,
.openStickerSets = false,
},
}),
});
const auto panel = _emojiPanel.get();
panel->setDesiredHeightValues(
1.,
st::emojiPanMinHeight / 2,
st::emojiPanMinHeight);
panel->hide();
panel->selector()->setCurrentPeer(peer);
panel->selector()->emojiChosen(
) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {
Ui::InsertEmojiAtCursor(_field->textCursor(), data.emoji);
}, lifetime());
panel->selector()->customEmojiChosen(
) | rpl::on_next([=](ChatHelpers::FileChosen data) {
const auto info = data.document->sticker();
if (info
&& info->setType == Data::StickersType::Emoji
&& !_show->session().premium()) {
ShowPremiumPreviewBox(
_show,
PremiumFeature::AnimatedEmoji);
} else {
Data::InsertCustomEmoji(_field, data.document);
}
}, lifetime());
_emojiToggle = Ui::CreateChild<Ui::EmojiButton>(_wrap.get(), st.emoji);
_emojiToggle->show();
_emojiToggle->installEventFilter(panel);
_emojiToggle->addClickHandler([=] {
panel->toggleAnimated();
});
_width.value(
) | rpl::filter(
rpl::mappers::_1 > 0
) | rpl::on_next([=](int newWidth) {
const auto fieldWidth = newWidth
- st::historySendPadding
- _emojiToggle->width()
- _send->width();
_field->resizeToWidth(fieldWidth);
_field->moveToLeft(
st::historySendPadding,
st::historySendPadding,
newWidth);
updateWrapSize(newWidth);
}, _lifetime);
rpl::combine(
_width.value(),
_field->heightValue()
) | rpl::on_next([=](int width, int height) {
if (width <= 0) {
return;
}
const auto minHeight = st::historySendSize.height()
- 2 * st::historySendPadding;
_send->moveToRight(0, height - minHeight, width);
_emojiToggle->moveToRight(_send->width(), height - minHeight, width);
updateWrapSize();
}, _lifetime);
_field->cancelled() | rpl::on_next([=] {
_closeRequests.fire({});
}, _lifetime);
const auto updateLimitPosition = [=](QSize parent, QSize label) {
const auto skip = st::historySendPadding;
return QPoint(parent.width() - label.width() - skip, skip);
};
Ui::AddLengthLimitLabel(_field, _limit, {
.customParent = _wrap.get(),
.customUpdatePosition = updateLimitPosition,
});
rpl::merge(
_field->submits() | rpl::to_empty,
_send->clicks() | rpl::to_empty
) | rpl::on_next([=] {
auto text = _field->getTextWithTags();
if (text.text.size() <= _limit) {
_submitted.fire(std::move(text));
}
}, _lifetime);
}
void MessageField::updateEmojiPanelGeometry() {
const auto global = _emojiToggle->mapToGlobal({ 0, 0 });
const auto local = _parent->mapFromGlobal(global);
_emojiPanel->moveBottomRight(
local.y(),
local.x() + _emojiToggle->width() * 3);
}
void MessageField::setupBackground() {
_wrap->paintRequest() | rpl::on_next([=] {
const auto radius = st::historySendSize.height() / 2.;
auto p = QPainter(_wrap.get());
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(st::storiesComposeBg);
p.drawRoundedRect(_wrap->rect(), radius, radius);
}, _lifetime);
}
void MessageField::resizeToWidth(int newWidth) {
_width = newWidth;
if (_wrap->isHidden()) {
Ui::SendPendingMoveResizeEvents(_wrap.get());
}
updateEmojiPanelGeometry();
}
void MessageField::move(int x, int y) {
_wrap->move(x, y);
if (_cache) {
_cache->move(x, y);
}
}
void MessageField::toggle(bool shown) {
if (_shown == shown) {
return;
} else if (shown) {
Assert(_width.current() > 0);
Ui::SendPendingMoveResizeEvents(_wrap.get());
} else if (Ui::InFocusChain(_field)) {
_parent->setFocus();
}
_shown = shown;
if (!anim::Disabled()) {
if (!_cache) {
auto image = Ui::GrabWidgetToImage(_wrap.get());
_cache = std::make_unique<Ui::RpWidget>(_parent);
const auto raw = _cache.get();
raw->paintRequest() | rpl::on_next([=] {
auto p = QPainter(raw);
auto hq = PainterHighQualityEnabler(p);
const auto scale = raw->height() / float64(_wrap->height());
const auto target = _wrap->rect();
const auto center = target.center();
p.translate(center);
p.scale(scale, scale);
p.translate(-center);
p.drawImage(target, image);
}, raw->lifetime());
raw->show();
raw->move(_wrap->pos());
raw->resize(_wrap->width(), 0);
_wrap->hide();
}
_shownAnimation.start(
[=] { shownAnimationCallback(); },
shown ? 0. : 1.,
shown ? 1. : 0.,
st::slideWrapDuration,
anim::easeOutCirc);
}
shownAnimationCallback();
}
void MessageField::raise() {
_wrap->raise();
if (_cache) {
_cache->raise();
}
if (_reactionPanel) {
_reactionPanel->raise();
}
if (_emojiPanel) {
_emojiPanel->raise();
}
}
void MessageField::updateWrapSize(int widthOverride) {
const auto width = widthOverride ? widthOverride : _wrap->width();
const auto height = _field->height() + 2 * st::historySendPadding;
_wrap->resize(width, height);
updateHeight();
}
void MessageField::updateHeight() {
_height = int(base::SafeRound(
_shownAnimation.value(_shown ? 1. : 0.) * _wrap->height()));
}
void MessageField::shownAnimationCallback() {
updateHeight();
if (_shownAnimation.animating()) {
Assert(_cache != nullptr);
_cache->resize(_cache->width(), _height.current());
_cache->update();
} else if (_shown) {
_cache = nullptr;
_wrap->show();
_field->setFocusFast();
} else {
_closed.fire({});
}
}
int MessageField::height() const {
return _height.current();
}
rpl::producer<int> MessageField::heightValue() const {
return _height.value();
}
rpl::producer<TextWithTags> MessageField::submitted() const {
return _submitted.events();
}
rpl::producer<> MessageField::closeRequests() const {
return _closeRequests.events();
}
rpl::producer<> MessageField::closed() const {
return _closed.events();
}
rpl::lifetime &MessageField::lifetime() {
return _lifetime;
}
} // namespace Calls::Group