mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-18 07:07:08 +02:00
Implement choose-reactions input field.
This commit is contained in:
parent
4ad70965e9
commit
1e26c33b3d
2 changed files with 270 additions and 81 deletions
Telegram/SourceFiles
|
@ -7,7 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
*/
|
||||
#include "boxes/peers/edit_peer_reactions.h"
|
||||
|
||||
#include "base/event_filter.h"
|
||||
#include "boxes/reactions_settings_box.h" // AddReactionAnimatedIcon
|
||||
#include "chat_helpers/tabbed_panel.h"
|
||||
#include "chat_helpers/tabbed_selector.h"
|
||||
#include "data/data_message_reactions.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_chat.h"
|
||||
|
@ -16,14 +19,252 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "main/main_session.h"
|
||||
#include "apiwrap.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/controls/emoji_button.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_settings.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_info.h"
|
||||
#include "styles/style_settings.h"
|
||||
|
||||
#include <QtWidgets/QTextEdit>
|
||||
#include <QtGui/QTextBlock>
|
||||
#include <QtGui/QTextDocumentFragment>
|
||||
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] QString AllowOnlyCustomEmojiProcessor(QStringView mimeTag) {
|
||||
auto all = TextUtilities::SplitTags(mimeTag);
|
||||
for (auto i = all.begin(); i != all.end();) {
|
||||
if (Ui::InputField::IsCustomEmojiLink(*i)) {
|
||||
++i;
|
||||
} else {
|
||||
i = all.erase(i);
|
||||
}
|
||||
}
|
||||
return TextUtilities::JoinTag(all);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool AllowOnlyCustomEmojiMimeDataHook(
|
||||
not_null<const QMimeData*> data,
|
||||
Ui::InputField::MimeAction action) {
|
||||
if (action == Ui::InputField::MimeAction::Check) {
|
||||
const auto textMime = TextUtilities::TagsTextMimeType();
|
||||
const auto tagsMime = TextUtilities::TagsMimeType();
|
||||
if (!data->hasFormat(textMime) || !data->hasFormat(tagsMime)) {
|
||||
return false;
|
||||
}
|
||||
auto text = QString::fromUtf8(data->data(textMime));
|
||||
auto tags = TextUtilities::DeserializeTags(
|
||||
data->data(tagsMime),
|
||||
text.size());
|
||||
auto checkedTill = 0;
|
||||
ranges::sort(tags, ranges::less(), &TextWithTags::Tag::offset);
|
||||
for (const auto &tag : tags) {
|
||||
if (tag.offset != checkedTill
|
||||
|| AllowOnlyCustomEmojiProcessor(tag.id) != tag.id) {
|
||||
return false;
|
||||
}
|
||||
checkedTill += tag.length;
|
||||
}
|
||||
return true;
|
||||
} else if (action == Ui::InputField::MimeAction::Insert) {
|
||||
return false;
|
||||
}
|
||||
Unexpected("Action in MimeData hook.");
|
||||
}
|
||||
|
||||
struct UniqueCustomEmojiContext {
|
||||
base::flat_set<uint64> ids;
|
||||
};
|
||||
|
||||
[[nodiscard]] bool RemoveNonCustomEmojiFragment(
|
||||
not_null<QTextDocument*> document,
|
||||
UniqueCustomEmojiContext &context) {
|
||||
context.ids.clear();
|
||||
auto removeFrom = 0;
|
||||
auto removeTill = 0;
|
||||
auto block = document->begin();
|
||||
for (auto j = block.begin(); !j.atEnd(); ++j) {
|
||||
const auto fragment = j.fragment();
|
||||
Assert(fragment.isValid());
|
||||
|
||||
removeTill = removeFrom = fragment.position();
|
||||
const auto format = fragment.charFormat();
|
||||
if (format.objectType() != Ui::InputField::kCustomEmojiFormat) {
|
||||
removeTill += fragment.length();
|
||||
break;
|
||||
}
|
||||
const auto id = format.property(Ui::InputField::kCustomEmojiId);
|
||||
if (!context.ids.emplace(id.toULongLong()).second) {
|
||||
removeTill += fragment.length();
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (removeTill == removeFrom) {
|
||||
block = block.next();
|
||||
if (block == document->end()) {
|
||||
return false;
|
||||
}
|
||||
removeTill = block.position();
|
||||
}
|
||||
Ui::PrepareFormattingOptimization(document);
|
||||
auto cursor = QTextCursor(document);
|
||||
cursor.setPosition(removeFrom);
|
||||
cursor.setPosition(removeTill, QTextCursor::KeepAnchor);
|
||||
cursor.removeSelectedText();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RemoveNonCustomEmoji(
|
||||
not_null<QTextDocument*> document,
|
||||
UniqueCustomEmojiContext &context) {
|
||||
if (!RemoveNonCustomEmojiFragment(document, context)) {
|
||||
return false;
|
||||
}
|
||||
while (RemoveNonCustomEmojiFragment(document, context)) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SetupOnlyCustomEmojiField(not_null<Ui::InputField*> field) {
|
||||
field->setTagMimeProcessor(AllowOnlyCustomEmojiProcessor);
|
||||
field->setMimeDataHook(AllowOnlyCustomEmojiMimeDataHook);
|
||||
|
||||
struct State {
|
||||
bool processing = false;
|
||||
bool pending = false;
|
||||
};
|
||||
const auto state = field->lifetime().make_state<State>();
|
||||
|
||||
field->changes(
|
||||
) | rpl::start_with_next([=] {
|
||||
state->pending = true;
|
||||
if (state->processing) {
|
||||
return;
|
||||
}
|
||||
auto context = UniqueCustomEmojiContext();
|
||||
auto changed = false;
|
||||
state->processing = true;
|
||||
while (state->pending) {
|
||||
state->pending = false;
|
||||
const auto document = field->rawTextEdit()->document();
|
||||
const auto pageSize = document->pageSize();
|
||||
QTextCursor(document).joinPreviousEditBlock();
|
||||
if (RemoveNonCustomEmoji(document, context)) {
|
||||
changed = true;
|
||||
}
|
||||
state->processing = false;
|
||||
QTextCursor(document).endEditBlock();
|
||||
if (document->pageSize() != pageSize) {
|
||||
document->setPageSize(pageSize);
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
field->forceProcessContentsChanges();
|
||||
}
|
||||
}, field->lifetime());
|
||||
}
|
||||
|
||||
struct ReactionsSelectorArgs {
|
||||
not_null<QWidget*> outer;
|
||||
not_null<Window::SessionController*> controller;
|
||||
rpl::producer<QString> title;
|
||||
std::vector<Data::Reaction> list;
|
||||
std::vector<Data::ReactionId> selected;
|
||||
Fn<void(std::vector<Data::ReactionId>)> callback;
|
||||
rpl::producer<> focusRequests;
|
||||
};
|
||||
|
||||
object_ptr<Ui::RpWidget> AddReactionsSelector(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
ReactionsSelectorArgs &&args) {
|
||||
using namespace ChatHelpers;
|
||||
|
||||
auto result = object_ptr<Ui::InputField>(
|
||||
parent,
|
||||
st::manageGroupReactionsField,
|
||||
Ui::InputField::Mode::MultiLine,
|
||||
std::move(args.title));
|
||||
const auto raw = result.data();
|
||||
|
||||
const auto customEmojiPaused = [controller = args.controller] {
|
||||
return controller->isGifPausedAtLeastFor(PauseReason::Layer);
|
||||
};
|
||||
raw->setCustomEmojiFactory(
|
||||
args.controller->session().data().customEmojiManager().factory(),
|
||||
std::move(customEmojiPaused));
|
||||
|
||||
SetupOnlyCustomEmojiField(raw);
|
||||
|
||||
std::move(args.focusRequests) | rpl::start_with_next([=] {
|
||||
raw->setFocusFast();
|
||||
}, raw->lifetime());
|
||||
|
||||
const auto toggle = Ui::CreateChild<Ui::EmojiButton>(
|
||||
parent.get(),
|
||||
st::boxAttachEmoji);
|
||||
|
||||
const auto panel = Ui::CreateChild<TabbedPanel>(
|
||||
args.outer.get(),
|
||||
args.controller,
|
||||
object_ptr<TabbedSelector>(
|
||||
nullptr,
|
||||
args.controller->uiShow(),
|
||||
Window::GifPauseReason::Layer,
|
||||
TabbedSelector::Mode::EmojiOnly));
|
||||
panel->setDesiredHeightValues(
|
||||
1.,
|
||||
st::emojiPanMinHeight / 2,
|
||||
st::emojiPanMinHeight);
|
||||
panel->hide();
|
||||
panel->selector()->customEmojiChosen(
|
||||
) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
|
||||
Data::InsertCustomEmoji(raw, data.document);
|
||||
}, panel->lifetime());
|
||||
|
||||
const auto updateEmojiPanelGeometry = [=] {
|
||||
const auto parent = panel->parentWidget();
|
||||
const auto global = toggle->mapToGlobal({ 0, 0 });
|
||||
const auto local = parent->mapFromGlobal(global);
|
||||
panel->moveBottomRight(
|
||||
local.y(),
|
||||
local.x() + toggle->width() * 3);
|
||||
};
|
||||
const auto scheduleUpdateEmojiPanelGeometry = [=] {
|
||||
// updateEmojiPanelGeometry uses not only container geometry, but
|
||||
// also container children geometries that will be updated later.
|
||||
crl::on_main(raw, updateEmojiPanelGeometry);
|
||||
};
|
||||
const auto filterCallback = [=](not_null<QEvent*> event) {
|
||||
const auto type = event->type();
|
||||
if (type == QEvent::Move || type == QEvent::Resize) {
|
||||
scheduleUpdateEmojiPanelGeometry();
|
||||
}
|
||||
return base::EventFilterResult::Continue;
|
||||
};
|
||||
base::install_event_filter(args.outer, filterCallback);
|
||||
|
||||
toggle->installEventFilter(panel);
|
||||
toggle->addClickHandler([=] {
|
||||
panel->toggleAnimated();
|
||||
});
|
||||
|
||||
raw->geometryValue() | rpl::start_with_next([=](QRect geometry) {
|
||||
toggle->move(
|
||||
geometry.x() + geometry.width() - toggle->width(),
|
||||
geometry.y() + geometry.height() - toggle->height());
|
||||
updateEmojiPanelGeometry();
|
||||
}, toggle->lifetime());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void EditAllowedReactionsBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
|
@ -48,6 +289,7 @@ void EditAllowedReactionsBox(
|
|||
rpl::variable<Option> option; // For groups.
|
||||
rpl::variable<bool> anyToggled; // For channels.
|
||||
rpl::event_stream<bool> forceToggleAll; // For channels.
|
||||
rpl::event_stream<> focusRequests;
|
||||
};
|
||||
const auto optionInitial = (allowed.type != AllowedReactionsType::Some)
|
||||
? Option::All
|
||||
|
@ -159,91 +401,33 @@ void EditAllowedReactionsBox(
|
|||
const auto reactions = wrap ? wrap->entity() : container.get();
|
||||
|
||||
Ui::AddSkip(reactions);
|
||||
Ui::AddSubsectionTitle(
|
||||
reactions,
|
||||
(enabled
|
||||
? tr::lng_manage_peer_reactions_available()
|
||||
: tr::lng_manage_peer_reactions_some_title()));
|
||||
|
||||
const auto like = QString::fromUtf8("\xf0\x9f\x91\x8d");
|
||||
const auto dislike = QString::fromUtf8("\xf0\x9f\x91\x8e");
|
||||
const auto activeOnStart = [&](const ReactionId &id) {
|
||||
const auto inSome = ranges::contains(allowed.some, id);
|
||||
if (!isGroup) {
|
||||
return inSome || (allowed.type != AllowedReactionsType::Some);
|
||||
}
|
||||
const auto emoji = id.emoji();
|
||||
const auto isDefault = (emoji == like) || (emoji == dislike);
|
||||
return (allowed.type != AllowedReactionsType::Some)
|
||||
? isDefault
|
||||
: (inSome || (isDefault && allowed.some.empty()));
|
||||
};
|
||||
const auto add = [&](const Reaction &entry) {
|
||||
const auto button = reactions->add(object_ptr<Ui::SettingsButton>(
|
||||
reactions,
|
||||
rpl::single(entry.title),
|
||||
st::manageGroupButton.button));
|
||||
AddReactionAnimatedIcon(
|
||||
button,
|
||||
button->sizeValue(
|
||||
) | rpl::map([=](const QSize &size) {
|
||||
return QPoint(
|
||||
st::editPeerReactionsIconLeft,
|
||||
(size.height() - iconHeight) / 2);
|
||||
}),
|
||||
iconHeight,
|
||||
entry,
|
||||
button->events(
|
||||
) | rpl::filter([=](not_null<QEvent*> event) {
|
||||
return event->type() == QEvent::Enter;
|
||||
}) | rpl::to_empty,
|
||||
rpl::never<>(),
|
||||
&button->lifetime());
|
||||
state->toggles.emplace(entry.id, button);
|
||||
button->toggleOn(rpl::single(
|
||||
activeOnStart(entry.id)
|
||||
) | rpl::then(enabled
|
||||
? (state->forceToggleAll.events() | rpl::type_erased())
|
||||
: rpl::never<bool>()
|
||||
));
|
||||
if (enabled) {
|
||||
button->toggledChanges(
|
||||
) | rpl::start_with_next([=](bool toggled) {
|
||||
if (toggled) {
|
||||
state->anyToggled = true;
|
||||
} else if (collect().some.empty()) {
|
||||
state->anyToggled = false;
|
||||
}
|
||||
}, button->lifetime());
|
||||
}
|
||||
};
|
||||
for (const auto &entry : list) {
|
||||
add(entry);
|
||||
auto selected = allowed.some;
|
||||
if (selected.empty()) {
|
||||
selected.push_back(Data::ReactionId(like));
|
||||
selected.push_back(Data::ReactionId(dislike));
|
||||
}
|
||||
for (const auto &id : allowed.some) {
|
||||
if (const auto customId = id.custom()) {
|
||||
// Some possible forward compatibility.
|
||||
const auto button = reactions->add(object_ptr<Ui::SettingsButton>(
|
||||
reactions,
|
||||
rpl::single(u"Custom reaction"_q),
|
||||
st::manageGroupButton.button));
|
||||
AddReactionCustomIcon(
|
||||
button,
|
||||
button->sizeValue(
|
||||
) | rpl::map([=](const QSize &size) {
|
||||
return QPoint(
|
||||
st::editPeerReactionsIconLeft,
|
||||
(size.height() - iconHeight) / 2);
|
||||
}),
|
||||
iconHeight,
|
||||
navigation->parentController(),
|
||||
customId,
|
||||
rpl::never<>(),
|
||||
&button->lifetime());
|
||||
state->toggles.emplace(id, button);
|
||||
button->toggleOn(rpl::single(true));
|
||||
const auto changed = [=](std::vector<Data::ReactionId> chosen) {
|
||||
};
|
||||
reactions->add(AddReactionsSelector(reactions, {
|
||||
.outer = box->getDelegate()->outerContainer(),
|
||||
.controller = navigation->parentController(),
|
||||
.title = (enabled
|
||||
? tr::lng_manage_peer_reactions_available()
|
||||
: tr::lng_manage_peer_reactions_some_title()),
|
||||
.list = list,
|
||||
.selected = std::move(selected),
|
||||
.callback = changed,
|
||||
.focusRequests = state->focusRequests.events(),
|
||||
}), st::boxRowPadding);
|
||||
|
||||
box->setFocusCallback([=] {
|
||||
if (!wrap || state->option.current() == Option::Some) {
|
||||
state->focusRequests.fire({});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
box->addButton(tr::lng_settings_save(), [=] {
|
||||
const auto result = collect();
|
||||
|
|
|
@ -601,6 +601,11 @@ manageDeleteGroupButton: SettingsCountButton(manageGroupNoIconButton) {
|
|||
}
|
||||
}
|
||||
|
||||
manageGroupReactionsField: InputField(defaultInputField) {
|
||||
textMargins: margins(1px, 26px, 31px, 4px);
|
||||
heightMax: 158px;
|
||||
}
|
||||
|
||||
infoEmptyFg: windowSubTextFg;
|
||||
infoEmptyPhoto: icon {{ "info/info_media_photo_empty", infoEmptyFg }};
|
||||
infoEmptyVideo: icon {{ "info/info_media_video_empty", infoEmptyFg }};
|
||||
|
|
Loading…
Add table
Reference in a new issue