init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
This commit is contained in:
542
Telegram/SourceFiles/boxes/reactions_settings_box.cpp
Normal file
542
Telegram/SourceFiles/boxes/reactions_settings_box.cpp
Normal file
@@ -0,0 +1,542 @@
|
||||
/*
|
||||
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 "boxes/reactions_settings_box.h"
|
||||
|
||||
#include "base/unixtime.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_document_media.h"
|
||||
#include "data/data_message_reactions.h"
|
||||
#include "data/data_session.h"
|
||||
#include "history/admin_log/history_admin_log_item.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "history/view/reactions/history_view_reactions_strip.h"
|
||||
#include "history/view/history_view_element.h"
|
||||
#include "history/view/history_view_fake_items.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "boxes/premium_preview_box.h"
|
||||
#include "main/main_session.h"
|
||||
#include "settings/settings_premium.h"
|
||||
#include "ui/chat/chat_style.h"
|
||||
#include "ui/chat/chat_theme.h"
|
||||
#include "ui/effects/scroll_content_shadow.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/animated_icon.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "window/section_widget.h"
|
||||
#include "window/themes/window_theme.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_media_player.h" // mediaPlayerMenuCheck
|
||||
#include "styles/style_settings.h"
|
||||
|
||||
namespace {
|
||||
|
||||
void AddMessage(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
not_null<Window::SessionController*> controller,
|
||||
rpl::producer<Data::ReactionId> &&idValue,
|
||||
int width) {
|
||||
|
||||
const auto widget = container->add(
|
||||
object_ptr<Ui::RpWidget>(container),
|
||||
style::margins(
|
||||
0,
|
||||
st::defaultVerticalListSkip,
|
||||
0,
|
||||
st::settingsPrivacySkipTop));
|
||||
|
||||
class Delegate final : public HistoryView::SimpleElementDelegate {
|
||||
public:
|
||||
using HistoryView::SimpleElementDelegate::SimpleElementDelegate;
|
||||
private:
|
||||
HistoryView::Context elementContext() override {
|
||||
return HistoryView::Context::ContactPreview;
|
||||
}
|
||||
};
|
||||
|
||||
struct State {
|
||||
AdminLog::OwnedItem reply;
|
||||
AdminLog::OwnedItem item;
|
||||
std::unique_ptr<Delegate> delegate;
|
||||
std::unique_ptr<Ui::ChatStyle> style;
|
||||
|
||||
struct {
|
||||
std::vector<rpl::lifetime> lifetimes;
|
||||
bool flag = false;
|
||||
} icons;
|
||||
};
|
||||
const auto state = container->lifetime().make_state<State>();
|
||||
state->delegate = std::make_unique<Delegate>(
|
||||
controller,
|
||||
crl::guard(widget, [=] { widget->update(); }));
|
||||
state->style = std::make_unique<Ui::ChatStyle>(
|
||||
controller->session().colorIndicesValue());
|
||||
state->style->apply(controller->defaultChatTheme().get());
|
||||
state->icons.lifetimes = std::vector<rpl::lifetime>(2);
|
||||
|
||||
const auto history = controller->session().data().history(
|
||||
PeerData::kServiceNotificationsId);
|
||||
state->reply = HistoryView::GenerateItem(
|
||||
state->delegate.get(),
|
||||
history,
|
||||
HistoryView::GenerateUser(
|
||||
history,
|
||||
tr::lng_settings_chat_message_reply_from(tr::now)),
|
||||
FullMsgId(),
|
||||
tr::lng_settings_chat_message_reply(tr::now));
|
||||
auto message = HistoryView::GenerateItem(
|
||||
state->delegate.get(),
|
||||
history,
|
||||
history->peer->id,
|
||||
state->reply->data()->fullId(),
|
||||
tr::lng_settings_chat_message(tr::now));
|
||||
const auto view = message.get();
|
||||
state->item = std::move(message);
|
||||
|
||||
const auto padding = st::settingsForwardPrivacyPadding;
|
||||
|
||||
const auto updateWidgetSize = [=](int width) {
|
||||
const auto height = view->resizeGetHeight(width);
|
||||
const auto top = view->marginTop();
|
||||
const auto bottom = view->marginBottom();
|
||||
const auto full = padding + top + height + bottom + padding;
|
||||
widget->resize(width, full);
|
||||
};
|
||||
widget->widthValue(
|
||||
) | rpl::filter(
|
||||
rpl::mappers::_1 >= (st::historyMinimalWidth / 2)
|
||||
) | rpl::on_next(updateWidgetSize, widget->lifetime());
|
||||
updateWidgetSize(width);
|
||||
|
||||
const auto rightSize = st::settingsReactionCornerSize;
|
||||
const auto rightRect = [=] {
|
||||
const auto viewInner = view->innerGeometry();
|
||||
return QRect(
|
||||
viewInner.x() + viewInner.width(),
|
||||
padding
|
||||
+ view->marginTop()
|
||||
+ view->resizeGetHeight(widget->width())
|
||||
- rightSize.height(),
|
||||
rightSize.width(),
|
||||
rightSize.height()).translated(st::settingsReactionCornerSkip);
|
||||
};
|
||||
|
||||
using ThemePtr = std::unique_ptr<Ui::ChatTheme>;
|
||||
const auto theme = widget->lifetime().make_state<ThemePtr>(
|
||||
Window::Theme::DefaultChatThemeOn(widget->lifetime()));
|
||||
widget->paintRequest(
|
||||
) | rpl::on_next([=](const QRect &rect) {
|
||||
Painter p(widget);
|
||||
p.setClipRect(rect);
|
||||
Window::SectionWidget::PaintBackground(
|
||||
p,
|
||||
theme->get(),
|
||||
QSize(widget->width(), widget->window()->height()),
|
||||
rect);
|
||||
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto theme = controller->defaultChatTheme().get();
|
||||
auto context = theme->preparePaintContext(
|
||||
state->style.get(),
|
||||
widget->rect(),
|
||||
widget->rect(),
|
||||
controller->isGifPausedAtLeastFor(
|
||||
Window::GifPauseReason::Layer));
|
||||
context.outbg = view->hasOutLayout();
|
||||
|
||||
{
|
||||
const auto radius = rightSize.height() / 2;
|
||||
const auto r = rightRect();
|
||||
const auto &st = context.st->messageStyle(
|
||||
context.outbg,
|
||||
context.selected());
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st.msgShadow);
|
||||
p.drawRoundedRect(r.translated(0, st::msgShadow), radius, radius);
|
||||
p.setBrush(st.msgBg);
|
||||
p.drawRoundedRect(r, radius, radius);
|
||||
}
|
||||
|
||||
p.translate(padding / 2, padding + view->marginBottom());
|
||||
view->draw(p, context);
|
||||
}, widget->lifetime());
|
||||
|
||||
auto selectedId = rpl::duplicate(idValue);
|
||||
std::move(
|
||||
selectedId
|
||||
) | rpl::on_next([
|
||||
=,
|
||||
idValue = std::move(idValue),
|
||||
iconSize = st::settingsReactionMessageSize
|
||||
](const Data::ReactionId &id) {
|
||||
const auto index = state->icons.flag ? 1 : 0;
|
||||
state->icons.flag = !state->icons.flag;
|
||||
state->icons.lifetimes[index] = rpl::lifetime();
|
||||
const auto &reactions = controller->session().data().reactions();
|
||||
auto iconPositionValue = widget->geometryValue(
|
||||
) | rpl::map([=](const QRect &r) {
|
||||
return widget->pos()
|
||||
+ rightRect().topLeft()
|
||||
+ QPoint(
|
||||
(rightSize.width() - iconSize) / 2,
|
||||
(rightSize.height() - iconSize) / 2);
|
||||
});
|
||||
auto destroys = rpl::duplicate(
|
||||
idValue
|
||||
) | rpl::skip(1) | rpl::to_empty;
|
||||
if (const auto customId = id.custom()) {
|
||||
AddReactionCustomIcon(
|
||||
container,
|
||||
std::move(iconPositionValue),
|
||||
iconSize,
|
||||
controller,
|
||||
customId,
|
||||
std::move(destroys),
|
||||
&state->icons.lifetimes[index]);
|
||||
return;
|
||||
}
|
||||
for (const auto &r : reactions.list(Data::Reactions::Type::Active)) {
|
||||
if (r.id != id) {
|
||||
continue;
|
||||
}
|
||||
AddReactionAnimatedIcon(
|
||||
container,
|
||||
std::move(iconPositionValue),
|
||||
iconSize,
|
||||
r,
|
||||
rpl::never<>(),
|
||||
std::move(destroys),
|
||||
&state->icons.lifetimes[index]);
|
||||
return;
|
||||
}
|
||||
}, widget->lifetime());
|
||||
}
|
||||
|
||||
not_null<Ui::RpWidget*> AddReactionIconWrap(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
rpl::producer<QPoint> iconPositionValue,
|
||||
int iconSize,
|
||||
Fn<void(not_null<QWidget*>, QPainter&)> paintCallback,
|
||||
rpl::producer<> &&destroys,
|
||||
not_null<rpl::lifetime*> stateLifetime) {
|
||||
struct State {
|
||||
base::unique_qptr<Ui::RpWidget> widget;
|
||||
Ui::Animations::Simple finalAnimation;
|
||||
};
|
||||
|
||||
const auto state = stateLifetime->make_state<State>();
|
||||
state->widget = base::make_unique_q<Ui::RpWidget>(parent);
|
||||
|
||||
const auto widget = state->widget.get();
|
||||
widget->resize(iconSize, iconSize);
|
||||
widget->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
std::move(
|
||||
iconPositionValue
|
||||
) | rpl::on_next([=](const QPoint &point) {
|
||||
widget->moveToLeft(point.x(), point.y());
|
||||
}, widget->lifetime());
|
||||
|
||||
const auto update = crl::guard(widget, [=] { widget->update(); });
|
||||
|
||||
widget->paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
auto p = QPainter(widget);
|
||||
|
||||
if (state->finalAnimation.animating()) {
|
||||
const auto progress = 1. - state->finalAnimation.value(0.);
|
||||
const auto size = widget->size();
|
||||
const auto scaledSize = size * progress;
|
||||
const auto scaledCenter = QPoint(
|
||||
(size.width() - scaledSize.width()) / 2.,
|
||||
(size.height() - scaledSize.height()) / 2.);
|
||||
p.setOpacity(progress);
|
||||
p.translate(scaledCenter);
|
||||
p.scale(progress, progress);
|
||||
}
|
||||
|
||||
paintCallback(widget, p);
|
||||
}, widget->lifetime());
|
||||
|
||||
std::move(
|
||||
destroys
|
||||
) | rpl::take(1) | rpl::on_next([=, from = 0., to = 1.] {
|
||||
state->finalAnimation.start(
|
||||
[=](float64 value) {
|
||||
update();
|
||||
if (value == to) {
|
||||
stateLifetime->destroy();
|
||||
}
|
||||
},
|
||||
from,
|
||||
to,
|
||||
st::defaultPopupMenu.showDuration);
|
||||
}, widget->lifetime());
|
||||
|
||||
widget->raise();
|
||||
widget->show();
|
||||
|
||||
return widget;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void AddReactionAnimatedIcon(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
rpl::producer<QPoint> iconPositionValue,
|
||||
int iconSize,
|
||||
const Data::Reaction &reaction,
|
||||
rpl::producer<> &&selects,
|
||||
rpl::producer<> &&destroys,
|
||||
not_null<rpl::lifetime*> stateLifetime) {
|
||||
struct State {
|
||||
struct Entry {
|
||||
std::shared_ptr<Data::DocumentMedia> media;
|
||||
std::shared_ptr<Ui::AnimatedIcon> icon;
|
||||
};
|
||||
Entry appear;
|
||||
Entry select;
|
||||
bool appearAnimated = false;
|
||||
rpl::lifetime loadingLifetime;
|
||||
};
|
||||
const auto state = stateLifetime->make_state<State>();
|
||||
|
||||
state->appear.media = reaction.appearAnimation->createMediaView();
|
||||
state->select.media = reaction.selectAnimation->createMediaView();
|
||||
state->appear.media->checkStickerLarge();
|
||||
state->select.media->checkStickerLarge();
|
||||
rpl::single() | rpl::then(
|
||||
reaction.appearAnimation->session().downloaderTaskFinished()
|
||||
) | rpl::on_next([=] {
|
||||
const auto check = [&](State::Entry &entry) {
|
||||
if (!entry.media) {
|
||||
return true;
|
||||
} else if (!entry.media->loaded()) {
|
||||
return false;
|
||||
}
|
||||
entry.icon = HistoryView::Reactions::DefaultIconFactory(
|
||||
entry.media.get(),
|
||||
iconSize);
|
||||
entry.media = nullptr;
|
||||
return true;
|
||||
};
|
||||
if (check(state->select) && check(state->appear)) {
|
||||
state->select.icon->setCustomEndFrame(1);
|
||||
state->select.icon->animate([] {});
|
||||
state->loadingLifetime.destroy();
|
||||
}
|
||||
}, state->loadingLifetime);
|
||||
|
||||
const auto paintCallback = [=](not_null<QWidget*> widget, QPainter &p) {
|
||||
const auto paintFrame = [&](not_null<Ui::AnimatedIcon*> animation) {
|
||||
const auto frame = animation->frame(st::windowFg->c);
|
||||
p.drawImage(
|
||||
QRect(
|
||||
(widget->width() - iconSize) / 2,
|
||||
(widget->height() - iconSize) / 2,
|
||||
iconSize,
|
||||
iconSize),
|
||||
frame);
|
||||
};
|
||||
|
||||
const auto appear = state->appear.icon.get();
|
||||
if (appear && !state->appearAnimated) {
|
||||
state->appearAnimated = true;
|
||||
appear->animate(crl::guard(widget, [=] { widget->update(); }));
|
||||
}
|
||||
if (appear && appear->animating()) {
|
||||
paintFrame(appear);
|
||||
if (appear->frameIndex() == appear->framesCount() - 1) {
|
||||
if (const auto select = state->select.icon.get()) {
|
||||
select->setCustomEndFrame(select->framesCount() - 1);
|
||||
}
|
||||
}
|
||||
} else if (const auto select = state->select.icon.get()) {
|
||||
paintFrame(select);
|
||||
}
|
||||
};
|
||||
const auto widget = AddReactionIconWrap(
|
||||
parent,
|
||||
std::move(iconPositionValue),
|
||||
iconSize,
|
||||
paintCallback,
|
||||
std::move(destroys),
|
||||
stateLifetime);
|
||||
|
||||
std::move(
|
||||
selects
|
||||
) | rpl::on_next([=] {
|
||||
const auto select = state->select.icon.get();
|
||||
if (select && !select->animating()) {
|
||||
select->animate(crl::guard(widget, [=] { widget->update(); }));
|
||||
}
|
||||
}, widget->lifetime());
|
||||
}
|
||||
|
||||
void AddReactionCustomIcon(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
rpl::producer<QPoint> iconPositionValue,
|
||||
int iconSize,
|
||||
not_null<Window::SessionController*> controller,
|
||||
DocumentId customId,
|
||||
rpl::producer<> &&destroys,
|
||||
not_null<rpl::lifetime*> stateLifetime) {
|
||||
struct State {
|
||||
std::unique_ptr<Ui::Text::CustomEmoji> custom;
|
||||
Fn<void()> repaint;
|
||||
};
|
||||
const auto state = stateLifetime->make_state<State>();
|
||||
static constexpr auto tag = Data::CustomEmojiManager::SizeTag::Normal;
|
||||
state->custom = controller->session().data().customEmojiManager().create(
|
||||
customId,
|
||||
[=] { state->repaint(); },
|
||||
tag);
|
||||
|
||||
const auto paintCallback = [=](not_null<QWidget*> widget, QPainter &p) {
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto size = Data::FrameSizeFromTag(tag) / ratio;
|
||||
state->custom->paint(p, {
|
||||
.textColor = st::windowFg->c,
|
||||
.now = crl::now(),
|
||||
.position = QPoint(
|
||||
(widget->width() - size) / 2,
|
||||
(widget->height() - size) / 2),
|
||||
.paused = controller->isGifPausedAtLeastFor(
|
||||
Window::GifPauseReason::Layer),
|
||||
});
|
||||
};
|
||||
const auto widget = AddReactionIconWrap(
|
||||
parent,
|
||||
std::move(iconPositionValue),
|
||||
iconSize,
|
||||
paintCallback,
|
||||
std::move(destroys),
|
||||
stateLifetime);
|
||||
state->repaint = crl::guard(widget, [=] { widget->update(); });
|
||||
}
|
||||
|
||||
void ReactionsSettingsBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<Window::SessionController*> controller) {
|
||||
|
||||
struct State {
|
||||
rpl::variable<Data::ReactionId> selectedId;
|
||||
};
|
||||
|
||||
const auto &reactions = controller->session().data().reactions();
|
||||
const auto state = box->lifetime().make_state<State>();
|
||||
state->selectedId = reactions.favoriteId();
|
||||
|
||||
const auto pinnedToTop = box->setPinnedToTopContent(
|
||||
object_ptr<Ui::VerticalLayout>(box));
|
||||
|
||||
auto idValue = state->selectedId.value();
|
||||
AddMessage(pinnedToTop, controller, std::move(idValue), box->width());
|
||||
|
||||
Ui::AddSubsectionTitle(
|
||||
pinnedToTop,
|
||||
tr::lng_settings_chat_reactions_subtitle());
|
||||
|
||||
const auto container = box->verticalLayout();
|
||||
|
||||
const auto check = Ui::CreateChild<Ui::RpWidget>(container.get());
|
||||
check->resize(st::settingsReactionCornerSize);
|
||||
check->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
check->paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
Painter p(check);
|
||||
st::mediaPlayerMenuCheck.paintInCenter(p, check->rect());
|
||||
}, check->lifetime());
|
||||
const auto checkButton = [=](not_null<const Ui::RpWidget*> button) {
|
||||
check->moveToRight(
|
||||
st::settingsButtonRightSkip,
|
||||
button->y() + (button->height() - check->height()) / 2);
|
||||
};
|
||||
|
||||
auto firstCheckedButton = (Ui::RpWidget*)(nullptr);
|
||||
auto list = reactions.list(Data::Reactions::Type::Active);
|
||||
if (const auto favorite = reactions.favorite()) {
|
||||
if (favorite->id.custom()) {
|
||||
list.insert(begin(list), *favorite);
|
||||
}
|
||||
}
|
||||
for (const auto &r : list) {
|
||||
const auto button = container->add(object_ptr<Ui::SettingsButton>(
|
||||
container,
|
||||
rpl::single<QString>(base::duplicate(r.title)),
|
||||
st::settingsButton));
|
||||
|
||||
const auto iconSize = st::settingsReactionSize;
|
||||
const auto left = button->st().iconLeft;
|
||||
auto iconPositionValue = button->sizeValue(
|
||||
) | rpl::map([=](const QSize &s) {
|
||||
return QPoint(
|
||||
left + st::settingsReactionRightSkip,
|
||||
(s.height() - iconSize) / 2);
|
||||
});
|
||||
if (const auto customId = r.id.custom()) {
|
||||
AddReactionCustomIcon(
|
||||
button,
|
||||
std::move(iconPositionValue),
|
||||
iconSize,
|
||||
controller,
|
||||
customId,
|
||||
rpl::never<>(),
|
||||
&button->lifetime());
|
||||
} else {
|
||||
AddReactionAnimatedIcon(
|
||||
button,
|
||||
std::move(iconPositionValue),
|
||||
iconSize,
|
||||
r,
|
||||
button->events(
|
||||
) | rpl::filter([=](not_null<QEvent*> event) {
|
||||
return event->type() == QEvent::Enter;
|
||||
}) | rpl::to_empty,
|
||||
rpl::never<>(),
|
||||
&button->lifetime());
|
||||
}
|
||||
button->setClickedCallback([=, id = r.id] {
|
||||
checkButton(button);
|
||||
state->selectedId = id;
|
||||
});
|
||||
if (r.id == state->selectedId.current()) {
|
||||
firstCheckedButton = button;
|
||||
}
|
||||
}
|
||||
if (firstCheckedButton) {
|
||||
firstCheckedButton->geometryValue(
|
||||
) | rpl::filter([=](const QRect &r) {
|
||||
return r.isValid();
|
||||
}) | rpl::take(1) | rpl::on_next([=] {
|
||||
checkButton(firstCheckedButton);
|
||||
}, firstCheckedButton->lifetime());
|
||||
}
|
||||
check->raise();
|
||||
|
||||
box->setTitle(tr::lng_settings_chat_reactions_title());
|
||||
box->setWidth(st::boxWideWidth);
|
||||
box->addButton(tr::lng_settings_save(), [=] {
|
||||
const auto &data = controller->session().data();
|
||||
const auto selectedId = state->selectedId.current();
|
||||
if (data.reactions().favoriteId() != selectedId) {
|
||||
data.reactions().setFavorite(selectedId);
|
||||
}
|
||||
box->closeBox();
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
Reference in New Issue
Block a user