mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-14 13:17:08 +02:00
Implement rich reactions selector.
This commit is contained in:
parent
1e26c33b3d
commit
43a8733fc7
17 changed files with 718 additions and 214 deletions
BIN
Telegram/Resources/icons/info/edit/stickers_add.png
Normal file
BIN
Telegram/Resources/icons/info/edit/stickers_add.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 470 B |
BIN
Telegram/Resources/icons/info/edit/stickers_add@2x.png
Normal file
BIN
Telegram/Resources/icons/info/edit/stickers_add@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 899 B |
BIN
Telegram/Resources/icons/info/edit/stickers_add@3x.png
Normal file
BIN
Telegram/Resources/icons/info/edit/stickers_add@3x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
|
@ -1347,6 +1347,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_manage_peer_reactions_none_about" = "Members of the group can't add any reactions to messages.";
|
||||
"lng_manage_peer_reactions_some_title" = "Only allow these reactions";
|
||||
"lng_manage_peer_reactions_available" = "Available reactions";
|
||||
"lng_manage_peer_reactions_own" = "You can also {link} emoji packs and use them as reactions.";
|
||||
"lng_manage_peer_reactions_own_link" = "create your own";
|
||||
"lng_manage_peer_reactions_level#one" = "Your channel needs to reach level **{count}** to use **{same_count}** custom reaction.";
|
||||
"lng_manage_peer_reactions_level#other" = "Your channel needs to reach level **{count}** to use **{same_count}** custom reactions.";
|
||||
"lng_manage_peer_reactions_boost" = "Boost your channel {link}.";
|
||||
"lng_manage_peer_reactions_boost_link" = "here";
|
||||
|
||||
"lng_manage_peer_antispam" = "Aggressive Anti-Spam";
|
||||
"lng_manage_peer_antispam_about" = "Telegram will filter more spam but may occasionally affect ordinary messages. You can report False Positives in Recent Actions.";
|
||||
|
@ -2087,6 +2093,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"lng_boost_channel_title_color" = "Enable colors";
|
||||
"lng_boost_channel_needs_level_color#one" = "Your channel needs to reach **Level {count}** to change channel color.";
|
||||
"lng_boost_channel_needs_level_color#other" = "Your channel needs to reach **Level {count}** to change channel color.";
|
||||
|
||||
"lng_boost_channel_title_reactions" = "Custom reactions";
|
||||
"lng_boost_channel_needs_level_reactions#one" = "Your channel needs to reach **Level {count}** to add **{same_count}** custom emoji as a reaction.";
|
||||
"lng_boost_channel_needs_level_reactions#other" = "Your channel needs to reach **Level {count}** to add **{same_count}** custom emoji as reactions.";
|
||||
|
||||
"lng_boost_channel_ask" = "Ask your **Premium** subscribers to boost your channel with this link:";
|
||||
"lng_boost_channel_ask_button" = "Copy Link";
|
||||
"lng_boost_channel_or" = "or";
|
||||
|
|
|
@ -527,7 +527,7 @@ void Apply(
|
|||
show->show(Box(Ui::AskBoostBox, Ui::AskBoostBoxData{
|
||||
.link = qs(data.vboost_url()),
|
||||
.boost = counters,
|
||||
.requiredLevel = required,
|
||||
.reason = { Ui::AskBoostChannelColor{ required } },
|
||||
}, openStatistics, nullptr));
|
||||
cancel();
|
||||
}).fail([=](const MTP::Error &error) {
|
||||
|
|
|
@ -22,6 +22,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "boxes/peers/edit_linked_chat_box.h"
|
||||
#include "boxes/peers/edit_peer_requests_box.h"
|
||||
#include "boxes/peers/edit_peer_reactions.h"
|
||||
#include "boxes/peers/replace_boost_box.h"
|
||||
#include "boxes/peer_list_controllers.h"
|
||||
#include "boxes/stickers_box.h"
|
||||
#include "boxes/username_box.h"
|
||||
#include "chat_helpers/emoji_suggestions_widget.h"
|
||||
|
@ -36,12 +38,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "data/data_peer_values.h"
|
||||
#include "data/data_user.h"
|
||||
#include "history/admin_log/history_admin_log_section.h"
|
||||
#include "info/boosts/info_boosts_widget.h"
|
||||
#include "info/profile/info_profile_values.h"
|
||||
#include "info/info_memento.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "mtproto/sender.h"
|
||||
#include "main/main_account.h"
|
||||
#include "main/main_app_config.h"
|
||||
#include "settings/settings_common.h" // IconDescriptor.
|
||||
#include "settings/settings_common.h"
|
||||
#include "ui/boxes/boost_box.h"
|
||||
#include "ui/controls/userpic_button.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/vertical_list.h"
|
||||
|
@ -255,6 +260,7 @@ private:
|
|||
Ui::VerticalLayout *buttonsLayout = nullptr;
|
||||
Ui::SettingsButton *forumToggle = nullptr;
|
||||
bool forumToggleLocked = false;
|
||||
bool levelRequested = false;
|
||||
Ui::SlideWrap<> *historyVisibilityWrap = nullptr;
|
||||
};
|
||||
struct Saving {
|
||||
|
@ -304,6 +310,7 @@ private:
|
|||
void submitDescription();
|
||||
void deleteWithConfirmation();
|
||||
void deleteChannel();
|
||||
void editReactions();
|
||||
|
||||
[[nodiscard]] std::optional<Saving> validate() const;
|
||||
[[nodiscard]] bool validateUsernamesOrder(Saving &to) const;
|
||||
|
@ -1100,36 +1107,21 @@ void Controller::fillManageSection() {
|
|||
return Data::PeerAllowedReactions(peer);
|
||||
});
|
||||
}) | rpl::flatten_latest();
|
||||
auto label = rpl::combine(
|
||||
std::move(allowedReactions),
|
||||
Info::Profile::FullReactionsCountValue(session)
|
||||
) | rpl::map([=](const Data::AllowedReactions &allowed, int total) {
|
||||
auto label = std::move(
|
||||
allowedReactions
|
||||
) | rpl::map([=](const Data::AllowedReactions &allowed) {
|
||||
const auto some = int(allowed.some.size());
|
||||
return (allowed.type != Data::AllowedReactionsType::Some)
|
||||
? tr::lng_manage_peer_reactions_on(tr::now)
|
||||
: some
|
||||
? (QString::number(some)
|
||||
+ " / "
|
||||
+ QString::number(std::max(some, total)))
|
||||
? QString::number(some)
|
||||
: tr::lng_manage_peer_reactions_off(tr::now);
|
||||
});
|
||||
const auto done = [=](const Data::AllowedReactions &chosen) {
|
||||
SaveAllowedReactions(_peer, chosen);
|
||||
};
|
||||
AddButtonWithCount(
|
||||
_controls.buttonsLayout,
|
||||
tr::lng_manage_peer_reactions(),
|
||||
std::move(label),
|
||||
[=] {
|
||||
_navigation->parentController()->show(Box(
|
||||
EditAllowedReactionsBox,
|
||||
_navigation,
|
||||
!_peer->isBroadcast(),
|
||||
session->data().reactions().list(
|
||||
Data::Reactions::Type::Active),
|
||||
Data::PeerAllowedReactions(_peer),
|
||||
done));
|
||||
},
|
||||
[=] { editReactions(); },
|
||||
{ &st::menuIconGroupReactions });
|
||||
}
|
||||
if (canEditPermissions) {
|
||||
|
@ -1277,6 +1269,61 @@ void Controller::fillManageSection() {
|
|||
}
|
||||
}
|
||||
|
||||
void Controller::editReactions() {
|
||||
const auto done = [=](const Data::AllowedReactions &chosen) {
|
||||
SaveAllowedReactions(_peer, chosen);
|
||||
};
|
||||
if (!_peer->isBroadcast()) {
|
||||
_navigation->uiShow()->show(Box(
|
||||
EditAllowedReactionsBox,
|
||||
EditAllowedReactionsArgs{
|
||||
.navigation = _navigation,
|
||||
.isGroup = true,
|
||||
.list = _navigation->session().data().reactions().list(
|
||||
Data::Reactions::Type::Active),
|
||||
.allowed = Data::PeerAllowedReactions(_peer),
|
||||
.save = done,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (_controls.levelRequested) {
|
||||
return;
|
||||
}
|
||||
_controls.levelRequested = true;
|
||||
_api.request(MTPpremium_GetBoostsStatus(
|
||||
_peer->input
|
||||
)).done([=](const MTPpremium_BoostsStatus &result) {
|
||||
_controls.levelRequested = false;
|
||||
const auto link = qs(result.data().vboost_url());
|
||||
const auto weak = base::make_weak(_navigation->parentController());
|
||||
auto counters = ParseBoostCounters(result);
|
||||
counters.mine = 0; // Don't show current level as just-reached.
|
||||
const auto askForBoosts = [=](int required) {
|
||||
if (const auto strong = weak.get()) {
|
||||
const auto openStatistics = [=, peer = _peer] {
|
||||
strong->showSection(Info::Boosts::Make(peer));
|
||||
};
|
||||
strong->show(Box(Ui::AskBoostBox, Ui::AskBoostBoxData{
|
||||
.link = link,
|
||||
.boost = counters,
|
||||
.reason = { Ui::AskBoostCustomReactions{ required } },
|
||||
}, openStatistics, nullptr));
|
||||
}
|
||||
};
|
||||
_navigation->uiShow()->show(Box(
|
||||
EditAllowedReactionsBox,
|
||||
EditAllowedReactionsArgs{
|
||||
.navigation = _navigation,
|
||||
.allowedCustomReactions = counters.level,
|
||||
.list = _navigation->session().data().reactions().list(
|
||||
Data::Reactions::Type::Active),
|
||||
.allowed = Data::PeerAllowedReactions(_peer),
|
||||
.askForBoosts = askForBoosts,
|
||||
.save = done,
|
||||
}));
|
||||
}).send();
|
||||
}
|
||||
|
||||
void Controller::fillPendingRequestsButton() {
|
||||
auto pendingRequestsCount = Info::Profile::MigratedOrMeValue(
|
||||
_peer
|
||||
|
|
|
@ -15,13 +15,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "data/data_peer.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_session.h"
|
||||
#include "history/view/reactions/history_view_reactions_selector.h"
|
||||
#include "main/main_session.h"
|
||||
#include "apiwrap.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/boxes/boost_box.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/controls/emoji_button.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
|
@ -30,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_info.h"
|
||||
#include "styles/style_settings.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
#include <QtWidgets/QTextEdit>
|
||||
#include <QtGui/QTextBlock>
|
||||
|
@ -37,6 +41,70 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
|
||||
namespace {
|
||||
|
||||
constexpr auto kDisabledEmojiOpacity = 0.4;
|
||||
|
||||
struct UniqueCustomEmojiContext {
|
||||
std::vector<DocumentId> ids;
|
||||
};
|
||||
|
||||
class MaybeDisabledEmoji final : public Ui::Text::CustomEmoji {
|
||||
public:
|
||||
MaybeDisabledEmoji(
|
||||
std::unique_ptr<CustomEmoji> wrapped,
|
||||
Fn<bool()> enabled);
|
||||
|
||||
int width() override;
|
||||
QString entityData() override;
|
||||
void paint(QPainter &p, const Context &context) override;
|
||||
void unload() override;
|
||||
bool ready() override;
|
||||
bool readyInDefaultState() override;
|
||||
|
||||
private:
|
||||
const std::unique_ptr<Ui::Text::CustomEmoji> _wrapped;
|
||||
const Fn<bool()> _enabled;
|
||||
|
||||
};
|
||||
|
||||
MaybeDisabledEmoji::MaybeDisabledEmoji(
|
||||
std::unique_ptr<CustomEmoji> wrapped,
|
||||
Fn<bool()> enabled)
|
||||
: _wrapped(std::move(wrapped))
|
||||
, _enabled(std::move(enabled)) {
|
||||
}
|
||||
|
||||
int MaybeDisabledEmoji::width() {
|
||||
return _wrapped->width();
|
||||
}
|
||||
|
||||
QString MaybeDisabledEmoji::entityData() {
|
||||
return _wrapped->entityData();
|
||||
}
|
||||
|
||||
void MaybeDisabledEmoji::paint(QPainter &p, const Context &context) {
|
||||
const auto disabled = !_enabled();
|
||||
const auto was = disabled ? p.opacity() : 1.;
|
||||
if (disabled) {
|
||||
p.setOpacity(kDisabledEmojiOpacity);
|
||||
}
|
||||
_wrapped->paint(p, context);
|
||||
if (disabled) {
|
||||
p.setOpacity(was);
|
||||
}
|
||||
}
|
||||
|
||||
void MaybeDisabledEmoji::unload() {
|
||||
_wrapped->unload();
|
||||
}
|
||||
|
||||
bool MaybeDisabledEmoji::ready() {
|
||||
return _wrapped->ready();
|
||||
}
|
||||
|
||||
bool MaybeDisabledEmoji::readyInDefaultState() {
|
||||
return _wrapped->readyInDefaultState();
|
||||
}
|
||||
|
||||
[[nodiscard]] QString AllowOnlyCustomEmojiProcessor(QStringView mimeTag) {
|
||||
auto all = TextUtilities::SplitTags(mimeTag);
|
||||
for (auto i = all.begin(); i != all.end();) {
|
||||
|
@ -78,9 +146,11 @@ namespace {
|
|||
Unexpected("Action in MimeData hook.");
|
||||
}
|
||||
|
||||
struct UniqueCustomEmojiContext {
|
||||
base::flat_set<uint64> ids;
|
||||
};
|
||||
[[nodiscard]] std::vector<Data::ReactionId> DefaultSelected() {
|
||||
const auto like = QString::fromUtf8("\xf0\x9f\x91\x8d");
|
||||
const auto dislike = QString::fromUtf8("\xf0\x9f\x91\x8e");
|
||||
return { Data::ReactionId{ like }, Data::ReactionId{ dislike } };
|
||||
}
|
||||
|
||||
[[nodiscard]] bool RemoveNonCustomEmojiFragment(
|
||||
not_null<QTextDocument*> document,
|
||||
|
@ -100,10 +170,12 @@ struct UniqueCustomEmojiContext {
|
|||
break;
|
||||
}
|
||||
const auto id = format.property(Ui::InputField::kCustomEmojiId);
|
||||
if (!context.ids.emplace(id.toULongLong()).second) {
|
||||
const auto documentId = id.toULongLong();
|
||||
if (ranges::contains(context.ids, documentId)) {
|
||||
removeTill += fragment.length();
|
||||
break;
|
||||
}
|
||||
context.ids.push_back(documentId);
|
||||
}
|
||||
while (removeTill == removeFrom) {
|
||||
block = block.next();
|
||||
|
@ -131,7 +203,9 @@ bool RemoveNonCustomEmoji(
|
|||
return true;
|
||||
}
|
||||
|
||||
void SetupOnlyCustomEmojiField(not_null<Ui::InputField*> field) {
|
||||
void SetupOnlyCustomEmojiField(
|
||||
not_null<Ui::InputField*> field,
|
||||
Fn<void(std::vector<DocumentId>)> callback) {
|
||||
field->setTagMimeProcessor(AllowOnlyCustomEmojiProcessor);
|
||||
field->setMimeDataHook(AllowOnlyCustomEmojiMimeDataHook);
|
||||
|
||||
|
@ -164,12 +238,54 @@ void SetupOnlyCustomEmojiField(not_null<Ui::InputField*> field) {
|
|||
document->setPageSize(pageSize);
|
||||
}
|
||||
}
|
||||
callback(context.ids);
|
||||
if (changed) {
|
||||
field->forceProcessContentsChanges();
|
||||
}
|
||||
}, field->lifetime());
|
||||
}
|
||||
|
||||
[[nodiscard]] TextWithTags ComposeEmojiList(
|
||||
not_null<Data::Reactions*> reactions,
|
||||
const std::vector<Data::ReactionId> &list) {
|
||||
auto result = TextWithTags();
|
||||
const auto size = [&] {
|
||||
return int(result.text.size());
|
||||
};
|
||||
auto added = base::flat_set<Data::ReactionId>();
|
||||
const auto &all = reactions->list(Data::Reactions::Type::All);
|
||||
const auto add = [&](Data::ReactionId id) {
|
||||
if (!added.emplace(id).second) {
|
||||
return;
|
||||
}
|
||||
auto unifiedId = id.custom();
|
||||
const auto offset = size();
|
||||
if (unifiedId) {
|
||||
result.text.append('@');
|
||||
} else {
|
||||
result.text.append(id.emoji());
|
||||
const auto i = ranges::find(all, id, &Data::Reaction::id);
|
||||
if (i == end(all)) {
|
||||
return;
|
||||
}
|
||||
unifiedId = i->selectAnimation->id;
|
||||
}
|
||||
const auto data = Data::SerializeCustomEmojiId(unifiedId);
|
||||
const auto tag = Ui::InputField::CustomEmojiLink(data);
|
||||
result.tags.append({ offset, size() - offset, tag });
|
||||
};
|
||||
for (const auto &id : list) {
|
||||
add(id);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
enum class ReactionsSelectorState {
|
||||
Active,
|
||||
Disabled,
|
||||
Hidden,
|
||||
};
|
||||
|
||||
struct ReactionsSelectorArgs {
|
||||
not_null<QWidget*> outer;
|
||||
not_null<Window::SessionController*> controller;
|
||||
|
@ -177,13 +293,16 @@ struct ReactionsSelectorArgs {
|
|||
std::vector<Data::Reaction> list;
|
||||
std::vector<Data::ReactionId> selected;
|
||||
Fn<void(std::vector<Data::ReactionId>)> callback;
|
||||
rpl::producer<> focusRequests;
|
||||
rpl::producer<ReactionsSelectorState> stateValue;
|
||||
int customAllowed = 0;
|
||||
bool all = false;
|
||||
};
|
||||
|
||||
object_ptr<Ui::RpWidget> AddReactionsSelector(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
ReactionsSelectorArgs &&args) {
|
||||
using namespace ChatHelpers;
|
||||
using HistoryView::Reactions::UnifiedFactoryOwner;
|
||||
|
||||
auto result = object_ptr<Ui::InputField>(
|
||||
parent,
|
||||
|
@ -191,23 +310,111 @@ object_ptr<Ui::RpWidget> AddReactionsSelector(
|
|||
Ui::InputField::Mode::MultiLine,
|
||||
std::move(args.title));
|
||||
const auto raw = result.data();
|
||||
const auto session = &args.controller->session();
|
||||
const auto owner = &session->data();
|
||||
const auto reactions = &owner->reactions();
|
||||
const auto customAllowed = args.customAllowed;
|
||||
|
||||
struct State {
|
||||
std::unique_ptr<Ui::RpWidget> overlay;
|
||||
std::unique_ptr<UnifiedFactoryOwner> unifiedFactoryOwner;
|
||||
UnifiedFactoryOwner::RecentFactory factory;
|
||||
base::flat_set<DocumentId> allowed;
|
||||
rpl::lifetime focusLifetime;
|
||||
};
|
||||
const auto state = raw->lifetime().make_state<State>();
|
||||
state->unifiedFactoryOwner = std::make_unique<UnifiedFactoryOwner>(
|
||||
session,
|
||||
reactions->list(Data::Reactions::Type::Active));
|
||||
state->factory = state->unifiedFactoryOwner->factory();
|
||||
|
||||
const auto customEmojiPaused = [controller = args.controller] {
|
||||
return controller->isGifPausedAtLeastFor(PauseReason::Layer);
|
||||
};
|
||||
raw->setCustomEmojiFactory(
|
||||
args.controller->session().data().customEmojiManager().factory(),
|
||||
std::move(customEmojiPaused));
|
||||
raw->setCustomEmojiFactory([=](QStringView data, Fn<void()> update)
|
||||
-> std::unique_ptr<Ui::Text::CustomEmoji> {
|
||||
const auto id = Data::ParseCustomEmojiData(data);
|
||||
auto result = owner->customEmojiManager().create(
|
||||
data,
|
||||
std::move(update));
|
||||
if (state->unifiedFactoryOwner->lookupReactionId(id).custom()) {
|
||||
return std::make_unique<MaybeDisabledEmoji>(
|
||||
std::move(result),
|
||||
[=] { return state->allowed.contains(id); });
|
||||
}
|
||||
using namespace Ui::Text;
|
||||
return std::make_unique<FirstFrameEmoji>(std::move(result));
|
||||
}, std::move(customEmojiPaused));
|
||||
|
||||
SetupOnlyCustomEmojiField(raw);
|
||||
const auto callback = args.callback;
|
||||
SetupOnlyCustomEmojiField(raw, [=](std::vector<DocumentId> ids) {
|
||||
auto allowed = base::flat_set<DocumentId>();
|
||||
auto reactions = std::vector<Data::ReactionId>();
|
||||
reactions.reserve(ids.size());
|
||||
allowed.reserve(std::min(customAllowed, int(ids.size())));
|
||||
const auto owner = state->unifiedFactoryOwner.get();
|
||||
for (const auto id : ids) {
|
||||
const auto reactionId = owner->lookupReactionId(id);
|
||||
if (reactionId.custom() && allowed.size() < customAllowed) {
|
||||
allowed.emplace(id);
|
||||
}
|
||||
reactions.push_back(reactionId);
|
||||
}
|
||||
if (state->allowed != allowed) {
|
||||
state->allowed = std::move(allowed);
|
||||
raw->rawTextEdit()->update();
|
||||
}
|
||||
callback(std::move(reactions));
|
||||
});
|
||||
raw->setTextWithTags(ComposeEmojiList(reactions, args.selected));
|
||||
|
||||
std::move(args.focusRequests) | rpl::start_with_next([=] {
|
||||
raw->setFocusFast();
|
||||
using SelectorState = ReactionsSelectorState;
|
||||
std::move(
|
||||
args.stateValue
|
||||
) | rpl::start_with_next([=](SelectorState value) {
|
||||
switch (value) {
|
||||
case SelectorState::Active:
|
||||
state->overlay = nullptr;
|
||||
state->focusLifetime.destroy();
|
||||
if (raw->empty()) {
|
||||
raw->setTextWithTags(
|
||||
ComposeEmojiList(reactions, DefaultSelected()));
|
||||
}
|
||||
raw->setDisabled(false);
|
||||
raw->setFocusFast();
|
||||
break;
|
||||
case SelectorState::Disabled:
|
||||
state->overlay = std::make_unique<Ui::RpWidget>(parent);
|
||||
state->overlay->show();
|
||||
raw->geometryValue() | rpl::start_with_next([=](QRect rect) {
|
||||
state->overlay->setGeometry(rect);
|
||||
}, state->overlay->lifetime());
|
||||
state->overlay->paintRequest() | rpl::start_with_next([=](QRect clip) {
|
||||
auto color = st::boxBg->c;
|
||||
color.setAlphaF(0.5);
|
||||
QPainter(state->overlay.get()).fillRect(
|
||||
clip,
|
||||
color);
|
||||
}, state->overlay->lifetime());
|
||||
[[fallthrough]];
|
||||
case SelectorState::Hidden:
|
||||
if (Ui::InFocusChain(raw)) {
|
||||
raw->parentWidget()->setFocus();
|
||||
}
|
||||
raw->setDisabled(true);
|
||||
raw->focusedChanges(
|
||||
) | rpl::start_with_next([=](bool focused) {
|
||||
if (focused) {
|
||||
raw->parentWidget()->setFocus();
|
||||
}
|
||||
}, state->focusLifetime);
|
||||
break;
|
||||
}
|
||||
}, raw->lifetime());
|
||||
|
||||
const auto toggle = Ui::CreateChild<Ui::EmojiButton>(
|
||||
const auto toggle = Ui::CreateChild<Ui::IconButton>(
|
||||
parent.get(),
|
||||
st::boxAttachEmoji);
|
||||
st::manageGroupReactions);
|
||||
|
||||
const auto panel = Ui::CreateChild<TabbedPanel>(
|
||||
args.outer.get(),
|
||||
|
@ -216,7 +423,11 @@ object_ptr<Ui::RpWidget> AddReactionsSelector(
|
|||
nullptr,
|
||||
args.controller->uiShow(),
|
||||
Window::GifPauseReason::Layer,
|
||||
TabbedSelector::Mode::EmojiOnly));
|
||||
(args.all
|
||||
? TabbedSelector::Mode::FullReactions
|
||||
: TabbedSelector::Mode::RecentReactions)));
|
||||
panel->selector()->provideRecentEmoji(
|
||||
state->unifiedFactoryOwner->unifiedIdsList());
|
||||
panel->setDesiredHeightValues(
|
||||
1.,
|
||||
st::emojiPanMinHeight / 2,
|
||||
|
@ -247,7 +458,13 @@ object_ptr<Ui::RpWidget> AddReactionsSelector(
|
|||
}
|
||||
return base::EventFilterResult::Continue;
|
||||
};
|
||||
base::install_event_filter(args.outer, filterCallback);
|
||||
for (auto widget = (QWidget*)raw
|
||||
; widget && widget != args.outer
|
||||
; widget = widget->parentWidget()) {
|
||||
base::install_event_filter(raw, widget, filterCallback);
|
||||
}
|
||||
base::install_event_filter(raw, args.outer, filterCallback);
|
||||
scheduleUpdateEmojiPanelGeometry();
|
||||
|
||||
toggle->installEventFilter(panel);
|
||||
toggle->addClickHandler([=] {
|
||||
|
@ -264,33 +481,105 @@ object_ptr<Ui::RpWidget> AddReactionsSelector(
|
|||
return result;
|
||||
}
|
||||
|
||||
void AddReactionsText(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
int allowedCustomReactions,
|
||||
rpl::producer<int> customCountValue,
|
||||
Fn<void(int required)> askForBoosts) {
|
||||
auto ownedInner = object_ptr<Ui::VerticalLayout>(container);
|
||||
const auto inner = ownedInner.data();
|
||||
const auto count = inner->lifetime().make_state<rpl::variable<int>>(
|
||||
std::move(customCountValue));
|
||||
|
||||
auto outer = container->add(
|
||||
object_ptr<Ui::DividerLabel>(
|
||||
container,
|
||||
std::move(ownedInner),
|
||||
st::defaultBoxDividerLabelPadding),
|
||||
QMargins(0, st::manageGroupReactionsTextSkip, 0, 0));
|
||||
const auto label = inner->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
inner,
|
||||
tr::lng_manage_peer_reactions_own(
|
||||
lt_link,
|
||||
tr::lng_manage_peer_reactions_own_link(
|
||||
) | Ui::Text::ToLink(),
|
||||
Ui::Text::WithEntities),
|
||||
st::boxDividerLabel));
|
||||
const auto weak = base::make_weak(navigation);
|
||||
label->setClickHandlerFilter([=](const auto &...) {
|
||||
if (const auto strong = weak.get()) {
|
||||
using Info = Window::SessionNavigation::PeerByLinkInfo;
|
||||
strong->showPeerByLink(Info{
|
||||
.usernameOrId = u"stickers"_q,
|
||||
.resolveType = Window::ResolveType::Mention,
|
||||
});
|
||||
}
|
||||
return false;
|
||||
});
|
||||
auto countString = count->value() | rpl::map([](int count) {
|
||||
return TextWithEntities{ QString::number(count) };
|
||||
});
|
||||
auto needs = rpl::combine(
|
||||
tr::lng_manage_peer_reactions_level(
|
||||
lt_count,
|
||||
count->value() | tr::to_count(),
|
||||
lt_same_count,
|
||||
std::move(countString),
|
||||
Ui::Text::RichLangValue),
|
||||
tr::lng_manage_peer_reactions_boost(
|
||||
lt_link,
|
||||
tr::lng_manage_peer_reactions_boost_link() | Ui::Text::ToLink(),
|
||||
Ui::Text::RichLangValue)
|
||||
) | rpl::map([](TextWithEntities &&a, TextWithEntities &&b) {
|
||||
a.append(' ').append(std::move(b));
|
||||
return std::move(a);
|
||||
});
|
||||
const auto wrap = inner->add(
|
||||
object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
|
||||
inner,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
inner,
|
||||
std::move(needs),
|
||||
st::boxDividerLabel),
|
||||
QMargins{ 0, st::normalFont->height, 0, 0 }));
|
||||
wrap->toggleOn(count->value() | rpl::map(
|
||||
rpl::mappers::_1 > allowedCustomReactions
|
||||
));
|
||||
wrap->finishAnimating();
|
||||
|
||||
wrap->entity()->setClickHandlerFilter([=](const auto &...) {
|
||||
askForBoosts(count->current());
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void EditAllowedReactionsBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
bool isGroup,
|
||||
const std::vector<Data::Reaction> &list,
|
||||
const Data::AllowedReactions &allowed,
|
||||
Fn<void(const Data::AllowedReactions &)> callback) {
|
||||
EditAllowedReactionsArgs &&args) {
|
||||
using namespace Data;
|
||||
using namespace rpl::mappers;
|
||||
|
||||
const auto iconHeight = st::editPeerReactionsPreview;
|
||||
box->setTitle(tr::lng_manage_peer_reactions());
|
||||
box->setWidth(st::boxWideWidth);
|
||||
|
||||
enum class Option {
|
||||
All,
|
||||
Some,
|
||||
None,
|
||||
};
|
||||
using SelectorState = ReactionsSelectorState;
|
||||
struct State {
|
||||
base::flat_map<ReactionId, not_null<Ui::SettingsButton*>> toggles;
|
||||
rpl::variable<Option> option; // For groups.
|
||||
rpl::variable<bool> anyToggled; // For channels.
|
||||
rpl::event_stream<bool> forceToggleAll; // For channels.
|
||||
rpl::event_stream<> focusRequests;
|
||||
rpl::variable<SelectorState> selectorState;
|
||||
std::vector<Data::ReactionId> selected;
|
||||
rpl::variable<int> customCount;
|
||||
};
|
||||
const auto allowed = args.allowed;
|
||||
const auto optionInitial = (allowed.type != AllowedReactionsType::Some)
|
||||
? Option::All
|
||||
: allowed.some.empty()
|
||||
|
@ -298,58 +587,24 @@ void EditAllowedReactionsBox(
|
|||
: Option::Some;
|
||||
const auto state = box->lifetime().make_state<State>(State{
|
||||
.option = optionInitial,
|
||||
.anyToggled = (optionInitial != Option::None),
|
||||
});
|
||||
|
||||
const auto collect = [=] {
|
||||
auto result = AllowedReactions();
|
||||
if (!isGroup || state->option.current() == Option::Some) {
|
||||
result.some.reserve(state->toggles.size());
|
||||
for (const auto &[id, button] : state->toggles) {
|
||||
if (button->toggled()) {
|
||||
result.some.push_back(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
result.type = isGroup
|
||||
? (state->option.current() != Option::All
|
||||
? AllowedReactionsType::Some
|
||||
: AllowedReactionsType::All)
|
||||
: (result.some.size() == state->toggles.size())
|
||||
? AllowedReactionsType::Default
|
||||
: AllowedReactionsType::Some;
|
||||
return result;
|
||||
};
|
||||
|
||||
const auto container = box->verticalLayout();
|
||||
|
||||
const auto isGroup = args.isGroup;
|
||||
const auto enabled = isGroup
|
||||
? nullptr
|
||||
: container->add(object_ptr<Ui::SettingsButton>(
|
||||
container,
|
||||
container.get(),
|
||||
tr::lng_manage_peer_reactions_enable(),
|
||||
st::manageGroupButton.button));
|
||||
if (enabled && !list.empty()) {
|
||||
AddReactionAnimatedIcon(
|
||||
enabled,
|
||||
enabled->sizeValue(
|
||||
) | rpl::map([=](const QSize &size) {
|
||||
return QPoint(
|
||||
st::manageGroupButton.iconPosition.x(),
|
||||
(size.height() - iconHeight) / 2);
|
||||
}),
|
||||
iconHeight,
|
||||
list.front(),
|
||||
rpl::never<>(),
|
||||
rpl::never<>(),
|
||||
&enabled->lifetime());
|
||||
}
|
||||
st::manageGroupNoIconButton.button));
|
||||
if (enabled) {
|
||||
enabled->toggleOn(state->anyToggled.value());
|
||||
enabled->toggledChanges(
|
||||
) | rpl::filter([=](bool value) {
|
||||
return (value != state->anyToggled.current());
|
||||
}) | rpl::start_to_stream(state->forceToggleAll, enabled->lifetime());
|
||||
enabled->toggleOn(rpl::single(optionInitial != Option::None));
|
||||
enabled->toggledValue(
|
||||
) | rpl::start_with_next([=](bool value) {
|
||||
state->selectorState = value
|
||||
? SelectorState::Active
|
||||
: SelectorState::Disabled;
|
||||
}, enabled->lifetime());
|
||||
}
|
||||
const auto group = std::make_shared<Ui::RadioenumGroup<Option>>(
|
||||
state->option.current());
|
||||
|
@ -395,44 +650,100 @@ void EditAllowedReactionsBox(
|
|||
container,
|
||||
object_ptr<Ui::VerticalLayout>(container)));
|
||||
if (wrap) {
|
||||
wrap->toggleOn(state->option.value() | rpl::map(_1 == Option::Some));
|
||||
wrap->toggleOn(state->option.value(
|
||||
) | rpl::map(_1 == Option::Some) | rpl::before_next([=](bool some) {
|
||||
if (!some) {
|
||||
state->selectorState = SelectorState::Hidden;
|
||||
}
|
||||
}) | rpl::after_next([=](bool some) {
|
||||
if (some) {
|
||||
state->selectorState = SelectorState::Active;
|
||||
}
|
||||
}));
|
||||
wrap->finishAnimating();
|
||||
}
|
||||
const auto reactions = wrap ? wrap->entity() : container.get();
|
||||
|
||||
Ui::AddSkip(reactions);
|
||||
|
||||
const auto like = QString::fromUtf8("\xf0\x9f\x91\x8d");
|
||||
const auto dislike = QString::fromUtf8("\xf0\x9f\x91\x8e");
|
||||
auto selected = allowed.some;
|
||||
if (selected.empty()) {
|
||||
selected.push_back(Data::ReactionId(like));
|
||||
selected.push_back(Data::ReactionId(dislike));
|
||||
}
|
||||
const auto all = args.list;
|
||||
auto selected = (allowed.type != AllowedReactionsType::Some)
|
||||
? (all
|
||||
| ranges::views::transform(&Data::Reaction::id)
|
||||
| ranges::to_vector)
|
||||
: allowed.some;
|
||||
const auto changed = [=](std::vector<Data::ReactionId> chosen) {
|
||||
state->selected = std::move(chosen);
|
||||
state->customCount = ranges::count_if(
|
||||
state->selected,
|
||||
&Data::ReactionId::custom);
|
||||
};
|
||||
changed(selected.empty() ? DefaultSelected() : std::move(selected));
|
||||
reactions->add(AddReactionsSelector(reactions, {
|
||||
.outer = box->getDelegate()->outerContainer(),
|
||||
.controller = navigation->parentController(),
|
||||
.controller = args.navigation->parentController(),
|
||||
.title = (enabled
|
||||
? tr::lng_manage_peer_reactions_available()
|
||||
: tr::lng_manage_peer_reactions_some_title()),
|
||||
.list = list,
|
||||
.selected = std::move(selected),
|
||||
.list = all,
|
||||
.selected = state->selected,
|
||||
.callback = changed,
|
||||
.focusRequests = state->focusRequests.events(),
|
||||
.stateValue = state->selectorState.value(),
|
||||
.customAllowed = args.allowedCustomReactions,
|
||||
.all = !args.isGroup,
|
||||
}), st::boxRowPadding);
|
||||
|
||||
box->setFocusCallback([=] {
|
||||
if (!wrap || state->option.current() == Option::Some) {
|
||||
state->focusRequests.fire({});
|
||||
state->selectorState.force_assign(SelectorState::Active);
|
||||
}
|
||||
});
|
||||
|
||||
if (!isGroup) {
|
||||
AddReactionsText(
|
||||
container,
|
||||
args.navigation,
|
||||
args.allowedCustomReactions,
|
||||
state->customCount.value(),
|
||||
args.askForBoosts);
|
||||
}
|
||||
const auto total = int(all.size());
|
||||
const auto collect = [=] {
|
||||
auto result = AllowedReactions();
|
||||
if (isGroup
|
||||
? (state->option.current() == Option::Some)
|
||||
: (enabled->toggled())) {
|
||||
result.some = state->selected;
|
||||
}
|
||||
auto some = result.some;
|
||||
auto simple = all | ranges::views::transform(
|
||||
&Data::Reaction::id
|
||||
) | ranges::to_vector;
|
||||
ranges::sort(some);
|
||||
ranges::sort(simple);
|
||||
result.type = isGroup
|
||||
? (state->option.current() != Option::All
|
||||
? AllowedReactionsType::Some
|
||||
: AllowedReactionsType::All)
|
||||
: (some == simple)
|
||||
? AllowedReactionsType::Default
|
||||
: AllowedReactionsType::Some;
|
||||
return result;
|
||||
};
|
||||
|
||||
box->addButton(tr::lng_settings_save(), [=] {
|
||||
const auto result = collect();
|
||||
if (!isGroup) {
|
||||
const auto custom = ranges::count_if(
|
||||
result.some,
|
||||
&Data::ReactionId::custom);
|
||||
if (custom > args.allowedCustomReactions) {
|
||||
args.askForBoosts(custom);
|
||||
return;
|
||||
}
|
||||
}
|
||||
box->closeBox();
|
||||
callback(result);
|
||||
args.save(result);
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] {
|
||||
box->closeBox();
|
||||
|
|
|
@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
*/
|
||||
#pragma once
|
||||
|
||||
class PeerData;
|
||||
#include "data/data_peer.h"
|
||||
|
||||
namespace Data {
|
||||
struct Reaction;
|
||||
|
@ -22,13 +22,19 @@ namespace Window {
|
|||
class SessionNavigation;
|
||||
} // namespace Window
|
||||
|
||||
struct EditAllowedReactionsArgs {
|
||||
not_null<Window::SessionNavigation*> navigation;
|
||||
int allowedCustomReactions = 0;
|
||||
bool isGroup = false;
|
||||
std::vector<Data::Reaction> list;
|
||||
Data::AllowedReactions allowed;
|
||||
Fn<void(int required)> askForBoosts;
|
||||
Fn<void(const Data::AllowedReactions &)> save;
|
||||
};
|
||||
|
||||
void EditAllowedReactionsBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<Window::SessionNavigation*> navigation,
|
||||
bool isGroup,
|
||||
const std::vector<Data::Reaction> &list,
|
||||
const Data::AllowedReactions &allowed,
|
||||
Fn<void(const Data::AllowedReactions &)> callback);
|
||||
EditAllowedReactionsArgs &&args);
|
||||
|
||||
void SaveAllowedReactions(
|
||||
not_null<PeerData*> peer,
|
||||
|
|
|
@ -785,6 +785,10 @@ void EmojiListWidget::unloadCustomIn(const SectionInfo &info) {
|
|||
object_ptr<TabbedSelector::InnerFooter> EmojiListWidget::createFooter() {
|
||||
Expects(_footer == nullptr);
|
||||
|
||||
if (_mode == EmojiListMode::RecentReactions) {
|
||||
return { nullptr };
|
||||
}
|
||||
|
||||
using FooterDescriptor = StickersListFooter::Descriptor;
|
||||
const auto flag = powerSavingFlag();
|
||||
const auto footerPaused = [method = pausedMethod(), flag]() {
|
||||
|
|
|
@ -331,8 +331,12 @@ TabbedSelector::TabbedSelector(
|
|||
Mode mode)
|
||||
: TabbedSelector(parent, {
|
||||
.show = std::move(show),
|
||||
.st = ((mode == Mode::EmojiStatus || mode == Mode::BackgroundEmoji)
|
||||
.st = ((mode == Mode::EmojiStatus
|
||||
|| mode == Mode::BackgroundEmoji
|
||||
|| mode == Mode::FullReactions)
|
||||
? st::statusEmojiPan
|
||||
: (mode == Mode::RecentReactions)
|
||||
? st::backgroundEmojiPan
|
||||
: st::defaultEmojiPan),
|
||||
.level = level,
|
||||
.mode = mode,
|
||||
|
@ -385,7 +389,11 @@ TabbedSelector::TabbedSelector(
|
|||
resize(st::emojiPanWidth, st::emojiPanMaxHeight);
|
||||
|
||||
for (auto &tab : _tabs) {
|
||||
tab.footer()->hide();
|
||||
if (tab.hasFooter()) {
|
||||
tab.footer()->hide();
|
||||
} else {
|
||||
_noFooter = true;
|
||||
}
|
||||
tab.widget()->hide();
|
||||
}
|
||||
if (tabbed()) {
|
||||
|
@ -515,6 +523,10 @@ TabbedSelector::Tab TabbedSelector::createTab(SelectorTab type, int index) {
|
|||
? EmojiMode::EmojiStatus
|
||||
: _mode == Mode::BackgroundEmoji
|
||||
? EmojiMode::BackgroundEmoji
|
||||
: _mode == Mode::FullReactions
|
||||
? EmojiMode::FullReactions
|
||||
: _mode == Mode::RecentReactions
|
||||
? EmojiMode::RecentReactions
|
||||
: EmojiMode::Full),
|
||||
.customTextColor = _customTextColor,
|
||||
.paused = paused,
|
||||
|
@ -694,10 +706,16 @@ void TabbedSelector::updateScrollGeometry(QSize oldSize) {
|
|||
}
|
||||
|
||||
void TabbedSelector::updateFooterGeometry() {
|
||||
_footerTop = _dropDown ? 0 : (height() - _st.footer);
|
||||
_footerTop = _dropDown
|
||||
? 0
|
||||
: _noFooter
|
||||
? (height() - _roundRadius)
|
||||
: (height() - _st.footer);
|
||||
for (auto &tab : _tabs) {
|
||||
tab.footer()->resizeToWidth(width());
|
||||
tab.footer()->moveToLeft(0, _footerTop);
|
||||
if (tab.hasFooter()) {
|
||||
tab.footer()->resizeToWidth(width());
|
||||
tab.footer()->moveToLeft(0, _footerTop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -767,7 +785,7 @@ void TabbedSelector::paintContent(QPainter &p) {
|
|||
0,
|
||||
_footerTop,
|
||||
width(),
|
||||
_st.footer);
|
||||
_noFooter ? _roundRadius : _st.footer);
|
||||
Ui::FillRoundRect(p, footerPart, footerBg, {
|
||||
.p = {
|
||||
_dropDown ? pixmaps.p[0] : QPixmap(),
|
||||
|
@ -802,7 +820,7 @@ void TabbedSelector::paintContent(QPainter &p) {
|
|||
}
|
||||
|
||||
int TabbedSelector::marginTop() const {
|
||||
return _dropDown
|
||||
return (_dropDown && !_noFooter)
|
||||
? _st.footer
|
||||
: _tabsSlider
|
||||
? (_tabsSlider->height() - st::lineWidth)
|
||||
|
@ -810,15 +828,19 @@ int TabbedSelector::marginTop() const {
|
|||
}
|
||||
|
||||
int TabbedSelector::scrollTop() const {
|
||||
return tabbed() ? marginTop() : _dropDown ? _st.footer : 0;
|
||||
return tabbed()
|
||||
? marginTop()
|
||||
: (_dropDown && !_noFooter)
|
||||
? _st.footer
|
||||
: 0;
|
||||
}
|
||||
|
||||
int TabbedSelector::marginBottom() const {
|
||||
return _dropDown ? _roundRadius : _st.footer;
|
||||
return (_dropDown || _noFooter) ? _roundRadius : _st.footer;
|
||||
}
|
||||
|
||||
int TabbedSelector::scrollBottom() const {
|
||||
return _dropDown ? 0 : marginBottom();
|
||||
return (_dropDown || _noFooter) ? 0 : marginBottom();
|
||||
}
|
||||
|
||||
void TabbedSelector::refreshStickers() {
|
||||
|
@ -1013,7 +1035,9 @@ void TabbedSelector::showAll() {
|
|||
if (isRestrictedView()) {
|
||||
_restrictedLabel->show();
|
||||
} else {
|
||||
currentTab()->footer()->show();
|
||||
if (currentTab()->hasFooter()) {
|
||||
currentTab()->footer()->show();
|
||||
}
|
||||
_scroll->show();
|
||||
_bottomShadow->setVisible(_mode == Mode::EmojiStatus);
|
||||
}
|
||||
|
@ -1100,7 +1124,7 @@ void TabbedSelector::fillTabsSliderSections() {
|
|||
}
|
||||
|
||||
bool TabbedSelector::hasSectionIcons() const {
|
||||
return !_restrictedLabel;
|
||||
return !_restrictedLabel && !_noFooter;
|
||||
}
|
||||
|
||||
void TabbedSelector::switchTab() {
|
||||
|
@ -1125,7 +1149,9 @@ void TabbedSelector::switchTab() {
|
|||
auto widget = _scroll->takeWidget<Inner>();
|
||||
widget->setParent(this);
|
||||
widget->hide();
|
||||
currentTab()->footer()->hide();
|
||||
if (currentTab()->hasFooter()) {
|
||||
currentTab()->footer()->hide();
|
||||
}
|
||||
currentTab()->returnWidget(std::move(widget));
|
||||
|
||||
_currentTabType = newTabType;
|
||||
|
|
|
@ -82,6 +82,8 @@ enum class TabbedSelectorMode {
|
|||
MediaEditor,
|
||||
EmojiStatus,
|
||||
BackgroundEmoji,
|
||||
FullReactions,
|
||||
RecentReactions,
|
||||
};
|
||||
|
||||
struct TabbedSelectorDescriptor {
|
||||
|
@ -192,16 +194,19 @@ private:
|
|||
object_ptr<Inner> takeWidget();
|
||||
void returnWidget(object_ptr<Inner> widget);
|
||||
|
||||
SelectorTab type() const {
|
||||
[[nodiscard]] SelectorTab type() const {
|
||||
return _type;
|
||||
}
|
||||
int index() const {
|
||||
[[nodiscard]] int index() const {
|
||||
return _index;
|
||||
}
|
||||
Inner *widget() const {
|
||||
[[nodiscard]] Inner *widget() const {
|
||||
return _weak;
|
||||
}
|
||||
not_null<InnerFooter*> footer() const {
|
||||
[[nodiscard]] bool hasFooter() const {
|
||||
return _footer != nullptr;
|
||||
}
|
||||
[[nodiscard]] not_null<InnerFooter*> footer() const {
|
||||
return _footer;
|
||||
}
|
||||
|
||||
|
@ -209,7 +214,7 @@ private:
|
|||
void saveScrollTop(int scrollTop) {
|
||||
_scrollTop = scrollTop;
|
||||
}
|
||||
int getScrollTop() const {
|
||||
[[nodiscard]] int getScrollTop() const {
|
||||
return _scrollTop;
|
||||
}
|
||||
|
||||
|
@ -279,6 +284,7 @@ private:
|
|||
Mode _mode = Mode::Full;
|
||||
int _roundRadius = 0;
|
||||
int _footerTop = 0;
|
||||
bool _noFooter = false;
|
||||
Ui::CornersPixmaps _panelRounding;
|
||||
Ui::CornersPixmaps _categoriesRounding;
|
||||
PeerData *_currentPeer = nullptr;
|
||||
|
|
|
@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/qt/qt_compare.h"
|
||||
|
||||
namespace Data {
|
||||
|
||||
struct ReactionId {
|
||||
|
@ -24,6 +26,13 @@ struct ReactionId {
|
|||
const auto custom = std::get_if<DocumentId>(&data);
|
||||
return custom ? *custom : DocumentId();
|
||||
}
|
||||
|
||||
friend inline auto operator<=>(
|
||||
const ReactionId &,
|
||||
const ReactionId &) = default;
|
||||
friend inline bool operator==(
|
||||
const ReactionId &a,
|
||||
const ReactionId &b) = default;
|
||||
};
|
||||
|
||||
struct MessageReaction {
|
||||
|
@ -32,13 +41,6 @@ struct MessageReaction {
|
|||
bool my = false;
|
||||
};
|
||||
|
||||
inline bool operator<(const ReactionId &a, const ReactionId &b) {
|
||||
return a.data < b.data;
|
||||
}
|
||||
inline bool operator==(const ReactionId &a, const ReactionId &b) {
|
||||
return a.data == b.data;
|
||||
}
|
||||
|
||||
[[nodiscard]] QString ReactionEntityData(const ReactionId &id);
|
||||
|
||||
[[nodiscard]] ReactionId ReactionFromMTP(const MTPReaction &reaction);
|
||||
|
|
|
@ -109,6 +109,87 @@ bool StripEmoji::readyInDefaultState() {
|
|||
|
||||
} // namespace
|
||||
|
||||
UnifiedFactoryOwner::UnifiedFactoryOwner(
|
||||
not_null<Main::Session*> session,
|
||||
const std::vector<Data::Reaction> &reactions,
|
||||
Strip *strip)
|
||||
: _session(session)
|
||||
, _strip(strip) {
|
||||
auto index = 0;
|
||||
const auto inStrip = _strip ? _strip->count() : 0;
|
||||
_unifiedIdsList.reserve(reactions.size());
|
||||
for (const auto &reaction : reactions) {
|
||||
if (const auto id = reaction.id.custom()) {
|
||||
_unifiedIdsList.push_back(id);
|
||||
} else {
|
||||
_unifiedIdsList.push_back(reaction.selectAnimation->id);
|
||||
}
|
||||
|
||||
const auto unifiedId = _unifiedIdsList.back();
|
||||
if (!reaction.id.custom()) {
|
||||
_defaultReactionIds.emplace(unifiedId, reaction.id.emoji());
|
||||
}
|
||||
if (index + 1 < inStrip) {
|
||||
_defaultReactionInStripMap.emplace(unifiedId, index++);
|
||||
}
|
||||
}
|
||||
|
||||
_stripPaintOneShift = [&] {
|
||||
// See EmojiListWidget custom emoji position resolving.
|
||||
const auto size = st::reactStripSize;
|
||||
const auto area = st::emojiPanArea;
|
||||
const auto areaPosition = QPoint(
|
||||
(size - area.width()) / 2,
|
||||
(size - area.height()) / 2);
|
||||
const auto esize = Ui::Emoji::GetSizeLarge() / style::DevicePixelRatio();
|
||||
const auto innerPosition = QPoint(
|
||||
(area.width() - esize) / 2,
|
||||
(area.height() - esize) / 2);
|
||||
const auto customSize = Ui::Text::AdjustCustomEmojiSize(esize);
|
||||
const auto customSkip = (esize - customSize) / 2;
|
||||
const auto customPosition = QPoint(customSkip, customSkip);
|
||||
return areaPosition + innerPosition + customPosition;
|
||||
}();
|
||||
|
||||
_defaultReactionShift = QPoint(
|
||||
(st::reactStripSize - st::reactStripImage) / 2,
|
||||
(st::reactStripSize - st::reactStripImage) / 2
|
||||
) - _stripPaintOneShift;
|
||||
}
|
||||
|
||||
Data::ReactionId UnifiedFactoryOwner::lookupReactionId(
|
||||
DocumentId unifiedId) const {
|
||||
const auto i = _defaultReactionIds.find(unifiedId);
|
||||
return (i != end(_defaultReactionIds))
|
||||
? Data::ReactionId{ i->second }
|
||||
: Data::ReactionId{ unifiedId };
|
||||
}
|
||||
|
||||
UnifiedFactoryOwner::RecentFactory UnifiedFactoryOwner::factory() {
|
||||
return [=](DocumentId id, Fn<void()> repaint)
|
||||
-> std::unique_ptr<Ui::Text::CustomEmoji> {
|
||||
const auto tag = Data::CustomEmojiManager::SizeTag::Large;
|
||||
const auto sizeOverride = st::reactStripImage;
|
||||
const auto isDefaultReaction = _defaultReactionIds.contains(id);
|
||||
const auto manager = &_session->data().customEmojiManager();
|
||||
auto result = isDefaultReaction
|
||||
? std::make_unique<Ui::Text::ShiftedEmoji>(
|
||||
manager->create(id, std::move(repaint), tag, sizeOverride),
|
||||
_defaultReactionShift)
|
||||
: manager->create(id, std::move(repaint), tag);
|
||||
const auto i = _defaultReactionInStripMap.find(id);
|
||||
if (i != end(_defaultReactionInStripMap)) {
|
||||
Assert(_strip != nullptr);
|
||||
return std::make_unique<StripEmoji>(
|
||||
std::move(result),
|
||||
_strip,
|
||||
-_stripPaintOneShift,
|
||||
i->second);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
Selector::Selector(
|
||||
not_null<QWidget*> parent,
|
||||
const style::EmojiPan &st,
|
||||
|
@ -749,65 +830,10 @@ void Selector::cacheExpandIcon() {
|
|||
|
||||
void Selector::createList() {
|
||||
using namespace ChatHelpers;
|
||||
auto recent = _recent;
|
||||
auto defaultReactionIds = base::flat_map<DocumentId, QString>();
|
||||
if (_strip) {
|
||||
recent.reserve(recentCount());
|
||||
auto index = 0;
|
||||
const auto inStrip = _strip->count();
|
||||
for (const auto &reaction : _reactions.recent) {
|
||||
if (const auto id = reaction.id.custom()) {
|
||||
recent.push_back(id);
|
||||
} else {
|
||||
recent.push_back(reaction.selectAnimation->id);
|
||||
defaultReactionIds.emplace(recent.back(), reaction.id.emoji());
|
||||
}
|
||||
if (index + 1 < inStrip) {
|
||||
_defaultReactionInStripMap.emplace(recent.back(), index++);
|
||||
}
|
||||
};
|
||||
}
|
||||
const auto manager = &_show->session().data().customEmojiManager();
|
||||
_stripPaintOneShift = [&] {
|
||||
// See EmojiListWidget custom emoji position resolving.
|
||||
const auto area = st::emojiPanArea;
|
||||
const auto areaPosition = QPoint(
|
||||
(_size - area.width()) / 2,
|
||||
(_size - area.height()) / 2);
|
||||
const auto esize = Ui::Emoji::GetSizeLarge() / style::DevicePixelRatio();
|
||||
const auto innerPosition = QPoint(
|
||||
(area.width() - esize) / 2,
|
||||
(area.height() - esize) / 2);
|
||||
const auto customSize = Ui::Text::AdjustCustomEmojiSize(esize);
|
||||
const auto customSkip = (esize - customSize) / 2;
|
||||
const auto customPosition = QPoint(customSkip, customSkip);
|
||||
return areaPosition + innerPosition + customPosition;
|
||||
}();
|
||||
_defaultReactionShift = QPoint(
|
||||
(_size - st::reactStripImage) / 2,
|
||||
(_size - st::reactStripImage) / 2
|
||||
) - _stripPaintOneShift;
|
||||
auto factory = [=](DocumentId id, Fn<void()> repaint)
|
||||
-> std::unique_ptr<Ui::Text::CustomEmoji> {
|
||||
const auto tag = Data::CustomEmojiManager::SizeTag::Large;
|
||||
const auto sizeOverride = st::reactStripImage;
|
||||
const auto isDefaultReaction = defaultReactionIds.contains(id);
|
||||
auto result = isDefaultReaction
|
||||
? std::make_unique<Ui::Text::ShiftedEmoji>(
|
||||
manager->create(id, std::move(repaint), tag, sizeOverride),
|
||||
_defaultReactionShift)
|
||||
: manager->create(id, std::move(repaint), tag);
|
||||
const auto i = _defaultReactionInStripMap.find(id);
|
||||
if (i != end(_defaultReactionInStripMap)) {
|
||||
Assert(_strip != nullptr);
|
||||
return std::make_unique<StripEmoji>(
|
||||
std::move(result),
|
||||
_strip.get(),
|
||||
-_stripPaintOneShift,
|
||||
i->second);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
_unifiedFactoryOwner = std::make_unique<UnifiedFactoryOwner>(
|
||||
&_show->session(),
|
||||
_strip ? _reactions.recent : std::vector<Data::Reaction>(),
|
||||
_strip.get());
|
||||
_scroll = Ui::CreateChild<Ui::ScrollArea>(this, st::reactPanelScroll);
|
||||
_scroll->hide();
|
||||
|
||||
|
@ -821,8 +847,10 @@ void Selector::createList() {
|
|||
.show = _show,
|
||||
.mode = _listMode,
|
||||
.paused = [] { return false; },
|
||||
.customRecentList = std::move(recent),
|
||||
.customRecentFactory = std::move(factory),
|
||||
.customRecentList = (_strip
|
||||
? _unifiedFactoryOwner->unifiedIdsList()
|
||||
: _recent),
|
||||
.customRecentFactory = _unifiedFactoryOwner->factory(),
|
||||
.st = st,
|
||||
})
|
||||
).data();
|
||||
|
@ -831,13 +859,8 @@ void Selector::createList() {
|
|||
|
||||
_list->customChosen(
|
||||
) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
|
||||
const auto id = DocumentId{ data.document->id };
|
||||
const auto i = defaultReactionIds.find(id);
|
||||
const auto reactionId = (i != end(defaultReactionIds))
|
||||
? Data::ReactionId{ i->second }
|
||||
: Data::ReactionId{ id };
|
||||
_chosen.fire({
|
||||
.id = reactionId,
|
||||
.id = _unifiedFactoryOwner->lookupReactionId(data.document->id),
|
||||
.icon = data.messageSendingFrom.frame,
|
||||
.globalGeometry = data.messageSendingFrom.globalStartGeometry,
|
||||
});
|
||||
|
|
|
@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "ui/rp_widget.h"
|
||||
|
||||
namespace Data {
|
||||
struct Reaction;
|
||||
struct ReactionId;
|
||||
} // namespace Data
|
||||
|
||||
|
@ -39,6 +40,39 @@ class PlainShadow;
|
|||
|
||||
namespace HistoryView::Reactions {
|
||||
|
||||
class UnifiedFactoryOwner final {
|
||||
public:
|
||||
using RecentFactory = Fn<std::unique_ptr<Ui::Text::CustomEmoji>(
|
||||
DocumentId,
|
||||
Fn<void()>)>;
|
||||
|
||||
UnifiedFactoryOwner(
|
||||
not_null<Main::Session*> session,
|
||||
const std::vector<Data::Reaction> &reactions,
|
||||
Strip *strip = nullptr);
|
||||
|
||||
[[nodiscard]] const std::vector<DocumentId> &unifiedIdsList() const {
|
||||
return _unifiedIdsList;
|
||||
}
|
||||
|
||||
[[nodiscard]] Data::ReactionId lookupReactionId(
|
||||
DocumentId unifiedId) const;
|
||||
|
||||
[[nodiscard]] RecentFactory factory();
|
||||
|
||||
private:
|
||||
const not_null<Main::Session*> _session;
|
||||
Strip *_strip = nullptr;
|
||||
|
||||
std::vector<DocumentId> _unifiedIdsList;
|
||||
base::flat_map<DocumentId, QString> _defaultReactionIds;
|
||||
base::flat_map<DocumentId, int> _defaultReactionInStripMap;
|
||||
|
||||
QPoint _defaultReactionShift;
|
||||
QPoint _stripPaintOneShift;
|
||||
|
||||
};
|
||||
|
||||
class Selector final : public Ui::RpWidget {
|
||||
public:
|
||||
Selector(
|
||||
|
@ -146,10 +180,7 @@ private:
|
|||
const std::vector<DocumentId> _recent;
|
||||
const ChatHelpers::EmojiListMode _listMode;
|
||||
Fn<void()> _jumpedToPremium;
|
||||
base::flat_map<DocumentId, int> _defaultReactionInStripMap;
|
||||
Ui::RoundAreaWithShadow _cachedRound;
|
||||
QPoint _defaultReactionShift;
|
||||
QPoint _stripPaintOneShift;
|
||||
std::unique_ptr<Strip> _strip;
|
||||
|
||||
rpl::event_stream<ChosenReaction> _chosen;
|
||||
|
@ -160,6 +191,7 @@ private:
|
|||
Ui::ScrollArea *_scroll = nullptr;
|
||||
ChatHelpers::EmojiListWidget *_list = nullptr;
|
||||
ChatHelpers::StickersListFooter *_footer = nullptr;
|
||||
std::unique_ptr<UnifiedFactoryOwner> _unifiedFactoryOwner;
|
||||
Ui::PlainShadow *_shadow = nullptr;
|
||||
rpl::variable<int> _shadowTop = 0;
|
||||
rpl::variable<int> _shadowSkip = 0;
|
||||
|
@ -175,7 +207,7 @@ private:
|
|||
QMargins _padding;
|
||||
int _specialExpandTopSkip = 0;
|
||||
int _collapsedTopSkip = 0;
|
||||
int _size = 0;
|
||||
const int _size = 0;
|
||||
int _recentRows = 0;
|
||||
int _columns = 0;
|
||||
int _skipx = 0;
|
||||
|
|
|
@ -601,10 +601,19 @@ manageDeleteGroupButton: SettingsCountButton(manageGroupNoIconButton) {
|
|||
}
|
||||
}
|
||||
|
||||
manageGroupReactions: IconButton(defaultIconButton) {
|
||||
width: 24px;
|
||||
height: 36px;
|
||||
icon: icon{{ "info/edit/stickers_add", historyComposeIconFg }};
|
||||
iconOver: icon{{ "info/edit/stickers_add", historyComposeIconFgOver }};
|
||||
}
|
||||
manageGroupReactionsField: InputField(defaultInputField) {
|
||||
textMargins: margins(1px, 26px, 31px, 4px);
|
||||
textMargins: margins(1px, 38px, 24px, 8px);
|
||||
placeholderShift: -32px;
|
||||
heightMin: 66px;
|
||||
heightMax: 158px;
|
||||
}
|
||||
manageGroupReactionsTextSkip: 16px;
|
||||
|
||||
infoEmptyFg: windowSubTextFg;
|
||||
infoEmptyPhoto: icon {{ "info/info_media_photo_empty", infoEmptyFg }};
|
||||
|
|
|
@ -467,12 +467,25 @@ void AskBoostBox(
|
|||
|
||||
box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });
|
||||
|
||||
auto title = tr::lng_boost_channel_title_color();
|
||||
auto text = rpl::combine(
|
||||
tr::lng_boost_channel_needs_level_color(
|
||||
auto title = v::is<AskBoostChannelColor>(data.reason.data)
|
||||
? tr::lng_boost_channel_title_color()
|
||||
: tr::lng_boost_channel_title_reactions();
|
||||
auto reasonText = v::match(data.reason.data, [&](
|
||||
AskBoostChannelColor data) {
|
||||
return tr::lng_boost_channel_needs_level_color(
|
||||
lt_count,
|
||||
rpl::single(float64(data.requiredLevel)),
|
||||
Ui::Text::RichLangValue),
|
||||
Ui::Text::RichLangValue);
|
||||
}, [&](AskBoostCustomReactions data) {
|
||||
return tr::lng_boost_channel_needs_level_reactions(
|
||||
lt_count,
|
||||
rpl::single(float64(data.count)),
|
||||
lt_same_count,
|
||||
rpl::single(TextWithEntities{ QString::number(data.count) }),
|
||||
Ui::Text::RichLangValue);
|
||||
});
|
||||
auto text = rpl::combine(
|
||||
std::move(reasonText),
|
||||
tr::lng_boost_channel_ask(Ui::Text::RichLangValue)
|
||||
) | rpl::map([](TextWithEntities &&text, TextWithEntities &&ask) {
|
||||
return text.append(u"\n\n"_q).append(std::move(ask));
|
||||
|
|
|
@ -50,10 +50,24 @@ void GiftForBoostsBox(
|
|||
void GiftedNoBoostsBox(not_null<GenericBox*> box);
|
||||
void PremiumForBoostsBox(not_null<GenericBox*> box, Fn<void()> buyPremium);
|
||||
|
||||
struct AskBoostChannelColor {
|
||||
int requiredLevel = 0;
|
||||
};
|
||||
|
||||
struct AskBoostCustomReactions {
|
||||
int count = 0;
|
||||
};
|
||||
|
||||
struct AskBoostReason {
|
||||
std::variant<
|
||||
AskBoostChannelColor,
|
||||
AskBoostCustomReactions> data;
|
||||
};
|
||||
|
||||
struct AskBoostBoxData {
|
||||
QString link;
|
||||
BoostCounters boost;
|
||||
int requiredLevel = 0;
|
||||
AskBoostReason reason;
|
||||
};
|
||||
|
||||
void AskBoostBox(
|
||||
|
|
Loading…
Add table
Reference in a new issue