From 4216d72c6761b4246678df95483f55a02bfd99f9 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Wed, 13 Apr 2022 00:05:21 +0300 Subject: [PATCH] Added ability to set favorite reaction from settings. --- Telegram/CMakeLists.txt | 2 + Telegram/Resources/langs/lang.strings | 6 + .../boxes/reactions_settings_box.cpp | 519 ++++++++++++++++++ .../boxes/reactions_settings_box.h | 20 + Telegram/SourceFiles/settings/settings.style | 6 + .../SourceFiles/settings/settings_chat.cpp | 30 + 6 files changed, 583 insertions(+) create mode 100644 Telegram/SourceFiles/boxes/reactions_settings_box.cpp create mode 100644 Telegram/SourceFiles/boxes/reactions_settings_box.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 97b1a2df40..2caed21170 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -256,6 +256,8 @@ PRIVATE boxes/phone_banned_box.h boxes/pin_messages_box.cpp boxes/pin_messages_box.h + boxes/reactions_settings_box.cpp + boxes/reactions_settings_box.h boxes/ringtones_box.cpp boxes/ringtones_box.h boxes/self_destruction_box.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index c716918549..d802bdbc4e 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -440,6 +440,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_chat_quick_action_reply" = "Reply with double click"; "lng_settings_chat_quick_action_react" = "Send reaction with double click"; +"lng_settings_chat_reactions_title" = "Quick Reaction"; +"lng_settings_chat_reactions_subtitle" = "Choose your favorite reaction"; +"lng_settings_chat_message_reply_from" = "Bob Harris"; +"lng_settings_chat_message_reply" = "Good morning"; +"lng_settings_chat_message" = "Do you know what time it is?"; + "lng_settings_section_filters" = "Folders"; "lng_settings_section_background" = "Chat background"; diff --git a/Telegram/SourceFiles/boxes/reactions_settings_box.cpp b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp new file mode 100644 index 0000000000..eeec040204 --- /dev/null +++ b/Telegram/SourceFiles/boxes/reactions_settings_box.cpp @@ -0,0 +1,519 @@ +/* +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_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_message.h" +#include "history/view/history_view_element.h" +#include "history/view/history_view_react_button.h" // DefaultIconFactory +#include "lang/lang_keys.h" +#include "lottie/lottie_icon.h" +#include "main/main_session.h" +#include "settings/settings_common.h" +#include "ui/chat/chat_style.h" +#include "ui/chat/chat_theme.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/fade_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "window/section_widget.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 { + +constexpr auto kVisibleButtonsCount = 7; + +void AddIcon( + not_null parent, + rpl::producer iconPositionValue, + int iconSize, + not_null session, + const Data::Reaction &reaction, + rpl::producer<> &&selects, + rpl::producer<> &&destroys, + not_null stateLifetime) { + + struct State { + struct Entry { + std::shared_ptr media; + std::shared_ptr icon; + }; + Entry appear; + Entry select; + bool appearAnimated = false; + rpl::lifetime loadingLifetime; + + base::unique_qptr widget; + + Ui::Animations::Simple finalAnimation; + }; + + const auto state = stateLifetime->make_state(); + state->widget = base::make_unique_q(parent); + + state->appear.media = reaction.appearAnimation->createMediaView(); + state->select.media = reaction.selectAnimation->createMediaView(); + state->appear.media->checkStickerLarge(); + state->select.media->checkStickerLarge(); + rpl::single() | rpl::then( + session->downloaderTaskFinished() + ) | rpl::start_with_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->loadingLifetime.destroy(); + } + }, state->loadingLifetime); + + const auto widget = state->widget.get(); + widget->resize(iconSize, iconSize); + widget->setAttribute(Qt::WA_TransparentForMouseEvents); + + std::move( + iconPositionValue + ) | rpl::start_with_next([=](const QPoint &point) { + widget->moveToLeft(point.x(), point.y()); + }, widget->lifetime()); + + const auto update = crl::guard(widget, [=] { widget->update(); }); + + widget->paintRequest( + ) | rpl::start_with_next([=, + frameSize = (iconSize / style::DevicePixelRatio())] { + Painter p(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); + } + + const auto paintFrame = [&](not_null animation) { + const auto frame = animation->frame(); + p.drawImage( + QRect( + (widget->width() - frameSize) / 2, + (widget->height() - frameSize) / 2, + frameSize, + frameSize), + frame); + }; + + const auto appear = state->appear.icon.get(); + if (appear && !state->appearAnimated) { + state->appearAnimated = true; + appear->animate(update, 0, appear->framesCount() - 1); + } + if (appear && appear->animating()) { + paintFrame(appear); + } else if (const auto select = state->select.icon.get()) { + paintFrame(select); + } + }, widget->lifetime()); + + std::move( + selects + ) | rpl::start_with_next([=] { + const auto select = state->select.icon.get(); + if (select && !select->animating()) { + select->animate(update, 0, select->framesCount() - 1); + } + }, widget->lifetime()); + + std::move( + destroys + ) | rpl::take(1) | rpl::start_with_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(); +} + +PeerId GenerateUser(not_null history, const QString &name) { + Expects(history->peer->isUser()); + const auto peerId = Data::FakePeerIdForJustName(name); + history->owner().processUser(MTP_user( + MTP_flags(MTPDuser::Flag::f_first_name | MTPDuser::Flag::f_min), + peerToBareMTPInt(peerId), + MTP_long(0), + MTP_string(tr::lng_settings_chat_message_reply_from(tr::now)), + MTPstring(), // last name + MTPstring(), // username + MTPstring(), // phone + MTPUserProfilePhoto(), // profile photo + MTPUserStatus(), // status + MTP_int(0), // bot info version + MTPVector(), // restrictions + MTPstring(), // bot placeholder + MTPstring())); // lang code + return peerId; +} + +AdminLog::OwnedItem GenerateItem( + not_null delegate, + not_null history, + PeerId from, + MsgId replyTo, + const QString &text) { + Expects(history->peer->isUser()); + + const auto item = history->addNewLocalMessage( + history->nextNonHistoryEntryId(), + MessageFlag::FakeHistoryItem + | MessageFlag::HasFromId + | MessageFlag::HasReplyInfo, + UserId(), // via + replyTo, + base::unixtime::now(), // date + from, + QString(), // postAuthor + TextWithEntities{ .text = text }, + MTP_messageMediaEmpty(), + HistoryMessageMarkupData(), + uint64(0)); // groupedId + + return AdminLog::OwnedItem(delegate, item); +} + +void AddMessage( + not_null box, + not_null controller, + rpl::producer &&emojiValue) { + + const auto widget = box->addRow( + object_ptr(box), + style::margins( + 0, + st::settingsSectionSkip, + 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; + std::unique_ptr style; + + struct { + std::vector lifetimes; + bool flag = false; + } icons; + }; + const auto state = box->lifetime().make_state(); + state->delegate = std::make_unique( + controller, + crl::guard(widget, [=] { widget->update(); })); + state->style = std::make_unique(); + state->icons.lifetimes = std::vector(2); + + const auto history = controller->session().data().history( + PeerData::kServiceNotificationsId); + state->reply = GenerateItem( + state->delegate.get(), + history, + GenerateUser( + history, + tr::lng_settings_chat_message_reply_from(tr::now)), + 0, + tr::lng_settings_chat_message_reply(tr::now)); + auto message = GenerateItem( + state->delegate.get(), + history, + history->peer->id, + state->reply->data()->fullId().msg, + tr::lng_settings_chat_message(tr::now)); + const auto view = message.get(); + state->item = std::move(message); + + const auto padding = st::settingsForwardPrivacyPadding; + + widget->widthValue( + ) | rpl::filter( + rpl::mappers::_1 >= (st::historyMinimalWidth / 2) + ) | rpl::start_with_next([=](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->lifetime()); + + 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); + }; + + widget->paintRequest( + ) | rpl::start_with_next([=](const QRect &rect) { + Window::SectionWidget::PaintBackground( + controller, + controller->defaultChatTheme().get(), // #TODO themes + widget, + rect); + + Painter p(widget); + auto hq = PainterHighQualityEnabler(p); + const auto theme = controller->defaultChatTheme().get(); + auto context = theme->preparePaintContext( + state->style.get(), + widget->rect(), + widget->rect()); + 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()); + + rpl::duplicate( + emojiValue + ) | rpl::start_with_next([=, + emojiValue = std::move(emojiValue), + iconSize = st::settingsReactionMessageSize]( + const QString &emoji) { + const auto &reactions = controller->session().data().reactions(); + for (const auto &r : reactions.list(Data::Reactions::Type::All)) { + if (emoji != r.emoji) { + continue; + } + const auto index = state->icons.flag ? 1 : 0; + state->icons.lifetimes[index] = rpl::lifetime(); + AddIcon( + box->verticalLayout(), + widget->geometryValue( + ) | rpl::map([=](const QRect &r) { + return widget->pos() + + rightRect().topLeft() + + QPoint( + (rightSize.width() - iconSize) / 2, + (rightSize.height() - iconSize) / 2); + }), + iconSize, + &controller->session(), + r, + rpl::never<>(), + rpl::duplicate(emojiValue) | rpl::skip(1) | rpl::to_empty, + &state->icons.lifetimes[index]); + state->icons.flag = !state->icons.flag; + return; + } + }, widget->lifetime()); +} + +void SetupShadows( + not_null parent, + not_null scroll, + not_null inner) { + using namespace rpl::mappers; + + const auto topShadow = Ui::CreateChild(parent.get()); + const auto bottomShadow = Ui::CreateChild(parent.get()); + scroll->geometryValue( + ) | rpl::start_with_next_done([=](const QRect &geometry) { + topShadow->resizeToWidth(geometry.width()); + topShadow->move( + geometry.x(), + geometry.y()); + bottomShadow->resizeToWidth(geometry.width()); + bottomShadow->move( + geometry.x(), + geometry.y() + geometry.height() - st::lineWidth); + }, [t = Ui::MakeWeak(topShadow), b = Ui::MakeWeak(bottomShadow)] { + Ui::DestroyChild(t.data()); + Ui::DestroyChild(b.data()); + }, topShadow->lifetime()); + + topShadow->toggleOn(scroll->scrollTopValue() | rpl::map(_1 > 0)); + bottomShadow->toggleOn(rpl::combine( + scroll->scrollTopValue(), + scroll->heightValue(), + inner->heightValue(), + _1 + _2 < _3)); +} + +} // namespace + +void ReactionsSettingsBox( + not_null box, + not_null controller) { + + struct State { + rpl::variable selectedEmoji; + }; + + const auto &reactions = controller->session().data().reactions(); + const auto state = box->lifetime().make_state(); + state->selectedEmoji = reactions.favorite(); + + AddMessage(box, controller, state->selectedEmoji.value()); + + const auto container = box->verticalLayout(); + Settings::AddSubsectionTitle( + container, + tr::lng_settings_chat_reactions_subtitle()); + + const auto &stButton = st::settingsButton; + const auto scrollContainer = box->addRow( + object_ptr( + box, + kVisibleButtonsCount + * (stButton.height + + stButton.padding.top() + + stButton.padding.bottom())), + style::margins()); + const auto scroll = Ui::CreateChild( + scrollContainer, + st::boxScroll); + const auto buttonsContainer = scroll->setOwnedWidget( + object_ptr(scroll)); + scrollContainer->sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + scroll->resize(s.width(), s.height()); + buttonsContainer->resizeToWidth(s.width()); + }, scroll->lifetime()); + + const auto check = Ui::CreateChild(buttonsContainer.get()); + check->resize(st::settingsReactionCornerSize); + check->setAttribute(Qt::WA_TransparentForMouseEvents); + check->paintRequest( + ) | rpl::start_with_next([=] { + Painter p(check); + st::mediaPlayerMenuCheck.paintInCenter(p, check->rect()); + }, check->lifetime()); + const auto checkButton = [=](not_null button) { + check->moveToRight( + st::settingsButtonRightSkip, + button->y() + (button->height() - check->height()) / 2); + }; + + auto firstCheckedButton = (Ui::RpWidget*)(nullptr); + for (const auto &r : reactions.list(Data::Reactions::Type::All)) { + const auto button = Settings::AddButton( + buttonsContainer, + rpl::single(base::duplicate(r.title)), + stButton); + + const auto iconSize = st::settingsReactionSize; + AddIcon( + button, + button->sizeValue( + ) | rpl::map([=, left = button->st().iconLeft](const QSize &s) { + return QPoint( + left + st::settingsReactionRightSkip, + (s.height() - iconSize) / 2); + }), + iconSize, + &controller->session(), + r, + button->events( + ) | rpl::filter([=](not_null event) { + return event->type() == QEvent::Enter; + }) | rpl::to_empty, + rpl::never<>(), + &button->lifetime()); + + button->setClickedCallback([=, emoji = r.emoji] { + checkButton(button); + state->selectedEmoji = emoji; + }); + if (r.emoji == state->selectedEmoji.current()) { + firstCheckedButton = button; + } + } + if (firstCheckedButton) { + firstCheckedButton->geometryValue( + ) | rpl::filter([=](const QRect &r) { + return r.isValid(); + }) | rpl::take(1) | rpl::start_with_next([=] { + checkButton(firstCheckedButton); + }, firstCheckedButton->lifetime()); + } + check->raise(); + + SetupShadows(scrollContainer, scroll, buttonsContainer); + + 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 selectedEmoji = state->selectedEmoji.current(); + if (data.reactions().favorite() != selectedEmoji) { + data.reactions().setFavorite(selectedEmoji); + } + box->closeBox(); + }); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); +} diff --git a/Telegram/SourceFiles/boxes/reactions_settings_box.h b/Telegram/SourceFiles/boxes/reactions_settings_box.h new file mode 100644 index 0000000000..db0d616da0 --- /dev/null +++ b/Telegram/SourceFiles/boxes/reactions_settings_box.h @@ -0,0 +1,20 @@ +/* +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 +*/ +#pragma once + +namespace Ui { +class GenericBox; +} // namespace Ui + +namespace Window { +class SessionController; +} // namespace Window + +void ReactionsSettingsBox( + not_null box, + not_null controller); diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index 45c88b180b..5c857d067d 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -370,6 +370,12 @@ settingsPeerToPeerSkip: 9px; settingsIconRadius: 6px; +settingsReactionSize: 50px; +settingsReactionRightSkip: -10px; +settingsReactionCornerSize: size(28px, 22px); +settingsReactionCornerSkip: point(11px, -6px); +settingsReactionMessageSize: 36px; + notifyPreviewMargins: margins(40px, 20px, 40px, 58px); notifyPreviewUserpicSize: 36px; notifyPreviewUserpicPosition: point(14px, 11px); diff --git a/Telegram/SourceFiles/settings/settings_chat.cpp b/Telegram/SourceFiles/settings/settings_chat.cpp index 1429457c4d..65a2914c61 100644 --- a/Telegram/SourceFiles/settings/settings_chat.cpp +++ b/Telegram/SourceFiles/settings/settings_chat.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_advanced.h" #include "boxes/connection_box.h" #include "boxes/auto_download_box.h" +#include "boxes/reactions_settings_box.h" #include "boxes/stickers_box.h" #include "ui/boxes/confirm_box.h" #include "boxes/background_box.h" @@ -62,6 +63,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mainwidget.h" #include "mainwindow.h" #include "facades.h" +#include "styles/style_chat_helpers.h" // stickersRemove #include "styles/style_settings.h" #include "styles/style_layers.h" #include "styles/style_window.h" @@ -836,6 +838,25 @@ void SetupMessages( const auto react = addQuick( Quick::React, tr::lng_settings_chat_quick_action_react(tr::now)); + + class EmptyButton final : public Ui::IconButton { + public: + EmptyButton(not_null p, const style::IconButton &st) + : Ui::IconButton(p, st) + , _rippleAreaPosition(st.rippleAreaPosition) { + } + protected: + void paintEvent(QPaintEvent *e) override { + Painter p(this); + + paintRipple(p, _rippleAreaPosition, nullptr); + } + private: + const QPoint _rippleAreaPosition; + }; + const auto buttonRight = Ui::CreateChild( + inner, + st::stickersRemove); const auto reactRight = Ui::CreateChild(inner); struct State { @@ -872,6 +893,11 @@ void SetupMessages( reactRight->moveToRight( st::settingsButtonRightSkip, r.y() + (r.height() - rightSize.height()) / 2); + buttonRight->moveToLeft( + reactRight->x() + + (rightSize.width() - buttonRight->width()) / 2, + reactRight->y() + + (rightSize.height() - buttonRight->height()) / 2); }, reactRight->lifetime()); groupQuick->setChangedCallback([=](Quick value) { @@ -879,6 +905,10 @@ void SetupMessages( Core::App().saveSettingsDelayed(); }); + buttonRight->setClickedCallback([=, show = Window::Show(controller)] { + show.showBox(Box(ReactionsSettingsBox, controller)); + }); + AddSkip(inner, st::settingsCheckboxesSkip); }