2022-09-06 16:53:42 +00:00
|
|
|
/*
|
|
|
|
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 "info/profile/info_profile_emoji_status_panel.h"
|
|
|
|
|
2023-10-27 19:40:39 +00:00
|
|
|
#include "api/api_peer_photo.h"
|
|
|
|
#include "apiwrap.h"
|
2022-09-06 16:53:42 +00:00
|
|
|
#include "data/data_user.h"
|
|
|
|
#include "data/data_session.h"
|
|
|
|
#include "data/data_document.h"
|
|
|
|
#include "data/data_emoji_statuses.h"
|
|
|
|
#include "lang/lang_keys.h"
|
|
|
|
#include "menu/menu_send.h" // SendMenu::Type.
|
|
|
|
#include "ui/boxes/confirm_box.h"
|
|
|
|
#include "ui/boxes/time_picker_box.h"
|
2022-10-06 13:06:18 +00:00
|
|
|
#include "ui/effects/emoji_fly_animation.h"
|
2022-09-06 16:53:42 +00:00
|
|
|
#include "ui/text/format_values.h"
|
|
|
|
#include "base/unixtime.h"
|
2022-09-14 10:45:23 +00:00
|
|
|
#include "boxes/premium_preview_box.h"
|
2022-09-06 16:53:42 +00:00
|
|
|
#include "window/window_session_controller.h"
|
|
|
|
#include "window/window_controller.h"
|
|
|
|
#include "main/main_session.h"
|
|
|
|
#include "mainwindow.h"
|
|
|
|
#include "chat_helpers/tabbed_panel.h"
|
|
|
|
#include "chat_helpers/tabbed_selector.h"
|
|
|
|
#include "styles/style_chat_helpers.h"
|
|
|
|
|
|
|
|
namespace Info::Profile {
|
|
|
|
namespace {
|
|
|
|
|
2022-09-22 08:27:46 +00:00
|
|
|
constexpr auto kLimitFirstRow = 8;
|
|
|
|
|
2022-09-06 16:53:42 +00:00
|
|
|
void PickUntilBox(not_null<Ui::GenericBox*> box, Fn<void(TimeId)> callback) {
|
|
|
|
box->setTitle(tr::lng_emoji_status_for_title());
|
|
|
|
|
|
|
|
const auto seconds = Ui::DefaultTimePickerValues();
|
|
|
|
const auto phrases = ranges::views::all(
|
|
|
|
seconds
|
|
|
|
) | ranges::views::transform(Ui::FormatMuteFor) | ranges::to_vector;
|
|
|
|
|
|
|
|
const auto pickerCallback = Ui::TimePickerBox(box, seconds, phrases, 0);
|
|
|
|
|
|
|
|
Ui::ConfirmBox(box, {
|
|
|
|
.confirmed = [=] {
|
|
|
|
callback(pickerCallback());
|
|
|
|
box->closeBox();
|
|
|
|
},
|
|
|
|
.confirmText = tr::lng_emoji_status_for_submit(),
|
|
|
|
.cancelText = tr::lng_cancel(),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
EmojiStatusPanel::EmojiStatusPanel() = default;
|
|
|
|
|
2024-04-26 07:10:12 +00:00
|
|
|
EmojiStatusPanel::~EmojiStatusPanel() {
|
|
|
|
if (hasFocus()) {
|
|
|
|
// Panel will try to return focus to the layer widget, the problem is
|
|
|
|
// we are destroying the layer widget probably right now and focusing
|
|
|
|
// it will lead to a crash, because it destroys its children (how we
|
|
|
|
// got here) after it clears focus out of itself. So if you return
|
|
|
|
// the focus inside a child destructor, it won't be cleared at all.
|
|
|
|
_panel->window()->setFocus();
|
|
|
|
}
|
|
|
|
}
|
2022-09-06 16:53:42 +00:00
|
|
|
|
2022-09-27 12:05:47 +00:00
|
|
|
void EmojiStatusPanel::setChooseFilter(Fn<bool(DocumentId)> filter) {
|
|
|
|
_chooseFilter = std::move(filter);
|
|
|
|
}
|
|
|
|
|
2022-09-06 16:53:42 +00:00
|
|
|
void EmojiStatusPanel::show(
|
|
|
|
not_null<Window::SessionController*> controller,
|
|
|
|
not_null<QWidget*> button,
|
|
|
|
Data::CustomEmojiSizeTag animationSizeTag) {
|
2023-10-27 19:40:39 +00:00
|
|
|
show({
|
|
|
|
.controller = controller,
|
|
|
|
.button = button,
|
|
|
|
.animationSizeTag = animationSizeTag,
|
2023-12-22 02:06:10 +00:00
|
|
|
.ensureAddedEmojiId = controller->session().user()->emojiStatusId(),
|
2023-10-27 19:40:39 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
void EmojiStatusPanel::show(Descriptor &&descriptor) {
|
|
|
|
const auto controller = descriptor.controller;
|
2022-09-06 16:53:42 +00:00
|
|
|
if (!_panel) {
|
2023-10-27 19:40:39 +00:00
|
|
|
create(descriptor);
|
2022-09-06 16:53:42 +00:00
|
|
|
|
|
|
|
_panel->shownValue(
|
|
|
|
) | rpl::filter([=] {
|
|
|
|
return (_panelButton != nullptr);
|
|
|
|
}) | rpl::start_with_next([=](bool shown) {
|
|
|
|
if (shown) {
|
|
|
|
_panelButton->installEventFilter(_panel.get());
|
|
|
|
} else {
|
|
|
|
_panelButton->removeEventFilter(_panel.get());
|
|
|
|
}
|
|
|
|
}, _panel->lifetime());
|
|
|
|
}
|
2023-10-27 19:40:39 +00:00
|
|
|
const auto button = descriptor.button;
|
2022-09-06 16:53:42 +00:00
|
|
|
if (const auto previous = _panelButton.data()) {
|
|
|
|
if (previous != button) {
|
|
|
|
previous->removeEventFilter(_panel.get());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_panelButton = button;
|
2023-10-27 19:40:39 +00:00
|
|
|
_animationSizeTag = descriptor.animationSizeTag;
|
2023-12-22 02:06:10 +00:00
|
|
|
const auto feed = [=, now = descriptor.ensureAddedEmojiId](
|
|
|
|
std::vector<DocumentId> list) {
|
|
|
|
list.insert(begin(list), 0);
|
|
|
|
if (now && !ranges::contains(list, now)) {
|
|
|
|
list.push_back(now);
|
|
|
|
}
|
|
|
|
_panel->selector()->provideRecentEmoji(list);
|
|
|
|
};
|
2023-10-27 19:40:39 +00:00
|
|
|
if (descriptor.backgroundEmojiMode) {
|
|
|
|
controller->session().api().peerPhoto().emojiListValue(
|
|
|
|
Api::PeerPhoto::EmojiListType::Background
|
|
|
|
) | rpl::start_with_next([=](std::vector<DocumentId> &&list) {
|
2023-12-22 02:06:10 +00:00
|
|
|
feed(std::move(list));
|
2023-10-27 19:40:39 +00:00
|
|
|
}, _panel->lifetime());
|
2023-12-22 02:06:10 +00:00
|
|
|
} else if (descriptor.channelStatusMode) {
|
|
|
|
const auto &statuses = controller->session().data().emojiStatuses();
|
|
|
|
const auto &other = statuses.list(Data::EmojiStatuses::Type::ChannelDefault);
|
|
|
|
auto list = statuses.list(Data::EmojiStatuses::Type::ChannelColored);
|
|
|
|
if (list.size() > kLimitFirstRow - 1) {
|
|
|
|
list.erase(begin(list) + kLimitFirstRow - 1, end(list));
|
|
|
|
}
|
|
|
|
list.reserve(list.size() + other.size() + 1);
|
|
|
|
for (const auto &id : other) {
|
|
|
|
if (!ranges::contains(list, id)) {
|
|
|
|
list.push_back(id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
feed(std::move(list));
|
2023-10-27 19:40:39 +00:00
|
|
|
} else {
|
|
|
|
const auto &statuses = controller->session().data().emojiStatuses();
|
|
|
|
const auto &recent = statuses.list(Data::EmojiStatuses::Type::Recent);
|
|
|
|
const auto &other = statuses.list(Data::EmojiStatuses::Type::Default);
|
|
|
|
auto list = statuses.list(Data::EmojiStatuses::Type::Colored);
|
2023-12-22 02:06:10 +00:00
|
|
|
if (list.size() > kLimitFirstRow - 1) {
|
|
|
|
list.erase(begin(list) + kLimitFirstRow - 1, end(list));
|
2023-10-27 19:40:39 +00:00
|
|
|
}
|
|
|
|
list.reserve(list.size() + recent.size() + other.size() + 1);
|
|
|
|
for (const auto &id : ranges::views::concat(recent, other)) {
|
|
|
|
if (!ranges::contains(list, id)) {
|
|
|
|
list.push_back(id);
|
|
|
|
}
|
|
|
|
}
|
2023-12-22 02:06:10 +00:00
|
|
|
feed(std::move(list));
|
2023-10-27 19:40:39 +00:00
|
|
|
}
|
2022-09-06 16:53:42 +00:00
|
|
|
const auto parent = _panel->parentWidget();
|
|
|
|
const auto global = button->mapToGlobal(QPoint());
|
|
|
|
const auto local = parent->mapFromGlobal(global);
|
2023-12-22 02:06:10 +00:00
|
|
|
if (descriptor.backgroundEmojiMode || descriptor.channelStatusMode) {
|
2023-10-27 19:40:39 +00:00
|
|
|
_panel->moveBottomRight(
|
|
|
|
local.y() + (st::normalFont->height / 2),
|
|
|
|
local.x() + button->width() * 3);
|
|
|
|
} else {
|
|
|
|
_panel->moveTopRight(
|
|
|
|
local.y() + button->height() - (st::normalFont->height / 2),
|
|
|
|
local.x() + button->width() * 3);
|
|
|
|
}
|
2022-09-06 16:53:42 +00:00
|
|
|
_panel->toggleAnimated();
|
|
|
|
}
|
|
|
|
|
2024-03-12 19:19:59 +00:00
|
|
|
bool EmojiStatusPanel::hasFocus() const {
|
|
|
|
return _panel && Ui::InFocusChain(_panel.get());
|
|
|
|
}
|
|
|
|
|
2023-10-27 19:40:39 +00:00
|
|
|
void EmojiStatusPanel::repaint() {
|
|
|
|
_panel->selector()->update();
|
|
|
|
}
|
|
|
|
|
2022-09-06 16:53:42 +00:00
|
|
|
bool EmojiStatusPanel::paintBadgeFrame(not_null<Ui::RpWidget*> widget) {
|
|
|
|
if (!_animation) {
|
|
|
|
return false;
|
|
|
|
} else if (_animation->paintBadgeFrame(widget)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
InvokeQueued(_animation->layer(), [=] { _animation = nullptr; });
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2023-10-27 19:40:39 +00:00
|
|
|
void EmojiStatusPanel::create(const Descriptor &descriptor) {
|
2022-09-06 16:53:42 +00:00
|
|
|
using Selector = ChatHelpers::TabbedSelector;
|
2023-10-27 19:40:39 +00:00
|
|
|
using Descriptor = ChatHelpers::TabbedSelectorDescriptor;
|
|
|
|
using Mode = ChatHelpers::TabbedSelector::Mode;
|
|
|
|
const auto controller = descriptor.controller;
|
2022-09-06 16:53:42 +00:00
|
|
|
const auto body = controller->window().widget()->bodyWidget();
|
|
|
|
_panel = base::make_unique_q<ChatHelpers::TabbedPanel>(
|
|
|
|
body,
|
|
|
|
controller,
|
|
|
|
object_ptr<Selector>(
|
|
|
|
nullptr,
|
2023-10-27 19:40:39 +00:00
|
|
|
Descriptor{
|
|
|
|
.show = controller->uiShow(),
|
2023-12-22 02:06:10 +00:00
|
|
|
.st = ((descriptor.backgroundEmojiMode
|
|
|
|
|| descriptor.channelStatusMode)
|
2023-10-27 19:40:39 +00:00
|
|
|
? st::backgroundEmojiPan
|
|
|
|
: st::statusEmojiPan),
|
|
|
|
.level = Window::GifPauseReason::Layer,
|
|
|
|
.mode = (descriptor.backgroundEmojiMode
|
|
|
|
? Mode::BackgroundEmoji
|
2023-12-22 02:06:10 +00:00
|
|
|
: descriptor.channelStatusMode
|
|
|
|
? Mode::ChannelStatus
|
2023-10-27 19:40:39 +00:00
|
|
|
: Mode::EmojiStatus),
|
|
|
|
.customTextColor = descriptor.customTextColor,
|
|
|
|
}));
|
|
|
|
_customTextColor = descriptor.customTextColor;
|
|
|
|
_backgroundEmojiMode = descriptor.backgroundEmojiMode;
|
2023-12-22 02:06:10 +00:00
|
|
|
_channelStatusMode = descriptor.channelStatusMode;
|
|
|
|
_panel->setDropDown(!_backgroundEmojiMode && !_channelStatusMode);
|
2022-09-06 16:53:42 +00:00
|
|
|
_panel->setDesiredHeightValues(
|
|
|
|
1.,
|
|
|
|
st::emojiPanMinHeight / 2,
|
|
|
|
st::emojiPanMinHeight);
|
|
|
|
_panel->hide();
|
|
|
|
|
|
|
|
struct Chosen {
|
|
|
|
DocumentId id = 0;
|
|
|
|
TimeId until = 0;
|
|
|
|
Ui::MessageSendingAnimationFrom animation;
|
|
|
|
};
|
|
|
|
|
|
|
|
_panel->selector()->contextMenuRequested(
|
|
|
|
) | rpl::start_with_next([=] {
|
|
|
|
_panel->selector()->showMenuWithType(SendMenu::Type::Scheduled);
|
|
|
|
}, _panel->lifetime());
|
|
|
|
|
|
|
|
auto statusChosen = _panel->selector()->customEmojiChosen(
|
2022-09-14 10:45:23 +00:00
|
|
|
) | rpl::map([=](ChatHelpers::FileChosen data) {
|
2022-09-06 16:53:42 +00:00
|
|
|
return Chosen{
|
|
|
|
.id = data.document->id,
|
|
|
|
.until = data.options.scheduled,
|
|
|
|
.animation = data.messageSendingFrom,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
auto emojiChosen = _panel->selector()->emojiChosen(
|
2022-09-14 10:45:23 +00:00
|
|
|
) | rpl::map([=](ChatHelpers::EmojiChosen data) {
|
2022-09-06 16:53:42 +00:00
|
|
|
return Chosen{ .animation = data.messageSendingFrom };
|
|
|
|
});
|
|
|
|
|
2023-12-22 02:06:10 +00:00
|
|
|
if (descriptor.backgroundEmojiMode || descriptor.channelStatusMode) {
|
2023-10-27 19:40:39 +00:00
|
|
|
rpl::merge(
|
|
|
|
std::move(statusChosen),
|
|
|
|
std::move(emojiChosen)
|
|
|
|
) | rpl::start_with_next([=](const Chosen &chosen) {
|
|
|
|
const auto owner = &controller->session().data();
|
2023-02-01 06:05:14 +00:00
|
|
|
startAnimation(owner, body, chosen.id, chosen.animation);
|
2023-12-22 02:06:10 +00:00
|
|
|
_someCustomChosen.fire({ chosen.id, chosen.until });
|
2022-09-12 05:51:55 +00:00
|
|
|
_panel->hideAnimated();
|
2023-10-27 19:40:39 +00:00
|
|
|
}, _panel->lifetime());
|
|
|
|
} else {
|
|
|
|
const auto weak = Ui::MakeWeak(_panel.get());
|
|
|
|
const auto accept = [=](Chosen chosen) {
|
|
|
|
Expects(chosen.until != Selector::kPickCustomTimeId);
|
|
|
|
|
|
|
|
// PickUntilBox calls this after EmojiStatusPanel is destroyed!
|
|
|
|
const auto owner = &controller->session().data();
|
|
|
|
if (weak) {
|
|
|
|
startAnimation(owner, body, chosen.id, chosen.animation);
|
|
|
|
}
|
|
|
|
owner->emojiStatuses().set(chosen.id, chosen.until);
|
|
|
|
};
|
|
|
|
|
|
|
|
rpl::merge(
|
|
|
|
std::move(statusChosen),
|
|
|
|
std::move(emojiChosen)
|
|
|
|
) | rpl::filter([=](const Chosen &chosen) {
|
|
|
|
return filter(controller, chosen.id);
|
|
|
|
}) | rpl::start_with_next([=](const Chosen &chosen) {
|
|
|
|
if (chosen.until == Selector::kPickCustomTimeId) {
|
|
|
|
_panel->hideAnimated();
|
|
|
|
controller->show(Box(PickUntilBox, [=](TimeId seconds) {
|
|
|
|
accept({ chosen.id, base::unixtime::now() + seconds });
|
|
|
|
}));
|
|
|
|
} else {
|
|
|
|
accept(chosen);
|
|
|
|
_panel->hideAnimated();
|
|
|
|
}
|
|
|
|
}, _panel->lifetime());
|
|
|
|
}
|
2022-09-06 16:53:42 +00:00
|
|
|
}
|
|
|
|
|
2022-09-27 12:05:47 +00:00
|
|
|
bool EmojiStatusPanel::filter(
|
|
|
|
not_null<Window::SessionController*> controller,
|
|
|
|
DocumentId chosenId) const {
|
|
|
|
if (_chooseFilter) {
|
|
|
|
return _chooseFilter(chosenId);
|
|
|
|
} else if (chosenId && !controller->session().premium()) {
|
2024-03-01 14:37:50 +00:00
|
|
|
ShowPremiumPreviewBox(controller, PremiumFeature::EmojiStatus);
|
2022-09-27 12:05:47 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-09-06 16:53:42 +00:00
|
|
|
void EmojiStatusPanel::startAnimation(
|
|
|
|
not_null<Data::Session*> owner,
|
|
|
|
not_null<Ui::RpWidget*> body,
|
|
|
|
DocumentId statusId,
|
|
|
|
Ui::MessageSendingAnimationFrom from) {
|
2022-10-04 17:03:55 +00:00
|
|
|
if (!_panelButton || !statusId) {
|
2022-09-06 16:53:42 +00:00
|
|
|
return;
|
|
|
|
}
|
2022-10-06 13:06:18 +00:00
|
|
|
auto args = Ui::ReactionFlyAnimationArgs{
|
2022-09-06 16:53:42 +00:00
|
|
|
.id = { { statusId } },
|
|
|
|
.flyIcon = from.frame,
|
|
|
|
.flyFrom = body->mapFromGlobal(from.globalStartGeometry),
|
2023-10-27 19:40:39 +00:00
|
|
|
.forceFirstFrame = _backgroundEmojiMode,
|
2022-09-06 16:53:42 +00:00
|
|
|
};
|
2023-10-27 19:40:39 +00:00
|
|
|
const auto color = _customTextColor
|
|
|
|
? _customTextColor
|
|
|
|
: [] { return st::profileVerifiedCheckBg->c; };
|
2022-10-06 13:06:18 +00:00
|
|
|
_animation = std::make_unique<Ui::EmojiFlyAnimation>(
|
2022-09-06 16:53:42 +00:00
|
|
|
body,
|
|
|
|
&owner->reactions(),
|
|
|
|
std::move(args),
|
|
|
|
[=] { _animation->repaint(); },
|
2023-10-27 19:40:39 +00:00
|
|
|
_customTextColor,
|
2022-09-06 16:53:42 +00:00
|
|
|
_animationSizeTag);
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace Info::Profile
|