mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-06-05 06:33:57 +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
|
@ -7,7 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
*/
|
*/
|
||||||
#include "boxes/peers/edit_peer_reactions.h"
|
#include "boxes/peers/edit_peer_reactions.h"
|
||||||
|
|
||||||
|
#include "base/event_filter.h"
|
||||||
#include "boxes/reactions_settings_box.h" // AddReactionAnimatedIcon
|
#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_message_reactions.h"
|
||||||
#include "data/data_peer.h"
|
#include "data/data_peer.h"
|
||||||
#include "data/data_chat.h"
|
#include "data/data_chat.h"
|
||||||
|
@ -16,14 +19,252 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "main/main_session.h"
|
#include "main/main_session.h"
|
||||||
#include "apiwrap.h"
|
#include "apiwrap.h"
|
||||||
#include "lang/lang_keys.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/layers/generic_box.h"
|
||||||
#include "ui/widgets/buttons.h"
|
#include "ui/widgets/buttons.h"
|
||||||
#include "ui/widgets/checkbox.h"
|
#include "ui/widgets/checkbox.h"
|
||||||
#include "ui/wrap/slide_wrap.h"
|
#include "ui/wrap/slide_wrap.h"
|
||||||
#include "ui/vertical_list.h"
|
#include "ui/vertical_list.h"
|
||||||
#include "window/window_session_controller.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_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(
|
void EditAllowedReactionsBox(
|
||||||
not_null<Ui::GenericBox*> box,
|
not_null<Ui::GenericBox*> box,
|
||||||
|
@ -48,6 +289,7 @@ void EditAllowedReactionsBox(
|
||||||
rpl::variable<Option> option; // For groups.
|
rpl::variable<Option> option; // For groups.
|
||||||
rpl::variable<bool> anyToggled; // For channels.
|
rpl::variable<bool> anyToggled; // For channels.
|
||||||
rpl::event_stream<bool> forceToggleAll; // For channels.
|
rpl::event_stream<bool> forceToggleAll; // For channels.
|
||||||
|
rpl::event_stream<> focusRequests;
|
||||||
};
|
};
|
||||||
const auto optionInitial = (allowed.type != AllowedReactionsType::Some)
|
const auto optionInitial = (allowed.type != AllowedReactionsType::Some)
|
||||||
? Option::All
|
? Option::All
|
||||||
|
@ -159,91 +401,33 @@ void EditAllowedReactionsBox(
|
||||||
const auto reactions = wrap ? wrap->entity() : container.get();
|
const auto reactions = wrap ? wrap->entity() : container.get();
|
||||||
|
|
||||||
Ui::AddSkip(reactions);
|
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 like = QString::fromUtf8("\xf0\x9f\x91\x8d");
|
||||||
const auto dislike = QString::fromUtf8("\xf0\x9f\x91\x8e");
|
const auto dislike = QString::fromUtf8("\xf0\x9f\x91\x8e");
|
||||||
const auto activeOnStart = [&](const ReactionId &id) {
|
auto selected = allowed.some;
|
||||||
const auto inSome = ranges::contains(allowed.some, id);
|
if (selected.empty()) {
|
||||||
if (!isGroup) {
|
selected.push_back(Data::ReactionId(like));
|
||||||
return inSome || (allowed.type != AllowedReactionsType::Some);
|
selected.push_back(Data::ReactionId(dislike));
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
for (const auto &id : allowed.some) {
|
const auto changed = [=](std::vector<Data::ReactionId> chosen) {
|
||||||
if (const auto customId = id.custom()) {
|
};
|
||||||
// Some possible forward compatibility.
|
reactions->add(AddReactionsSelector(reactions, {
|
||||||
const auto button = reactions->add(object_ptr<Ui::SettingsButton>(
|
.outer = box->getDelegate()->outerContainer(),
|
||||||
reactions,
|
.controller = navigation->parentController(),
|
||||||
rpl::single(u"Custom reaction"_q),
|
.title = (enabled
|
||||||
st::manageGroupButton.button));
|
? tr::lng_manage_peer_reactions_available()
|
||||||
AddReactionCustomIcon(
|
: tr::lng_manage_peer_reactions_some_title()),
|
||||||
button,
|
.list = list,
|
||||||
button->sizeValue(
|
.selected = std::move(selected),
|
||||||
) | rpl::map([=](const QSize &size) {
|
.callback = changed,
|
||||||
return QPoint(
|
.focusRequests = state->focusRequests.events(),
|
||||||
st::editPeerReactionsIconLeft,
|
}), st::boxRowPadding);
|
||||||
(size.height() - iconHeight) / 2);
|
|
||||||
}),
|
box->setFocusCallback([=] {
|
||||||
iconHeight,
|
if (!wrap || state->option.current() == Option::Some) {
|
||||||
navigation->parentController(),
|
state->focusRequests.fire({});
|
||||||
customId,
|
|
||||||
rpl::never<>(),
|
|
||||||
&button->lifetime());
|
|
||||||
state->toggles.emplace(id, button);
|
|
||||||
button->toggleOn(rpl::single(true));
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
box->addButton(tr::lng_settings_save(), [=] {
|
box->addButton(tr::lng_settings_save(), [=] {
|
||||||
const auto result = collect();
|
const auto result = collect();
|
||||||
|
|
|
@ -601,6 +601,11 @@ manageDeleteGroupButton: SettingsCountButton(manageGroupNoIconButton) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
manageGroupReactionsField: InputField(defaultInputField) {
|
||||||
|
textMargins: margins(1px, 26px, 31px, 4px);
|
||||||
|
heightMax: 158px;
|
||||||
|
}
|
||||||
|
|
||||||
infoEmptyFg: windowSubTextFg;
|
infoEmptyFg: windowSubTextFg;
|
||||||
infoEmptyPhoto: icon {{ "info/info_media_photo_empty", infoEmptyFg }};
|
infoEmptyPhoto: icon {{ "info/info_media_photo_empty", infoEmptyFg }};
|
||||||
infoEmptyVideo: icon {{ "info/info_media_video_empty", infoEmptyFg }};
|
infoEmptyVideo: icon {{ "info/info_media_video_empty", infoEmptyFg }};
|
||||||
|
|
Loading…
Add table
Reference in a new issue