From ab0d2bf9c68d016a71aab248b1173dbf0454eca3 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 24 Sep 2021 19:10:25 +0400 Subject: [PATCH] Initial chat theme changing. --- Telegram/CMakeLists.txt | 2 + Telegram/Resources/langs/lang.strings | 8 + .../SourceFiles/data/data_cloud_themes.cpp | 30 +- Telegram/SourceFiles/data/data_cloud_themes.h | 8 +- Telegram/SourceFiles/data/data_peer.cpp | 15 +- Telegram/SourceFiles/data/data_peer.h | 4 +- .../SourceFiles/history/history_widget.cpp | 188 +++++--- Telegram/SourceFiles/history/history_widget.h | 31 +- Telegram/SourceFiles/mainwidget.cpp | 4 + Telegram/SourceFiles/mainwidget.h | 2 + Telegram/SourceFiles/ui/chat/chat_theme.cpp | 5 + Telegram/SourceFiles/ui/chat/chat_theme.h | 2 +- .../ui/chat/choose_theme_controller.cpp | 424 ++++++++++++++++++ .../ui/chat/choose_theme_controller.h | 73 +++ .../SourceFiles/window/section_widget.cpp | 25 +- .../window/themes/window_theme.cpp | 11 + .../SourceFiles/window/themes/window_theme.h | 2 + .../SourceFiles/window/window_peer_menu.cpp | 5 + .../window/window_session_controller.cpp | 33 +- .../window/window_session_controller.h | 21 +- 20 files changed, 773 insertions(+), 120 deletions(-) create mode 100644 Telegram/SourceFiles/ui/chat/choose_theme_controller.cpp create mode 100644 Telegram/SourceFiles/ui/chat/choose_theme_controller.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 5f0e135f7..d9133905d 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1040,6 +1040,8 @@ PRIVATE ui/chat/attach/attach_item_single_file_preview.h ui/chat/attach/attach_item_single_media_preview.cpp ui/chat/attach/attach_item_single_media_preview.h + ui/chat/choose_theme_controller.cpp + ui/chat/choose_theme_controller.h ui/effects/fireworks_animation.cpp ui/effects/fireworks_animation.h ui/effects/round_checkbox.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index b59129ef6..d66174c5a 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2871,6 +2871,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_filters_remove_sure" = "This will remove the folder, your chats will not be deleted."; "lng_filters_remove_yes" = "Remove"; +"lng_chat_theme_change" = "Change colors"; +"lng_chat_theme_none" = "No\nTheme"; +"lng_chat_theme_apply" = "Apply Theme"; +"lng_chat_theme_reset" = "Reset Theme"; +"lng_chat_theme_dont" = "Do Not Set Theme"; +"lng_chat_theme_title" = "Select theme"; +"lng_chat_theme_cant_voice" = "Sorry, you can't change the chat theme while you're having an unsent voice message."; + "lng_photo_editor_menu_delete" = "Delete"; "lng_photo_editor_menu_flip" = "Flip"; "lng_photo_editor_menu_duplicate" = "Duplicate"; diff --git a/Telegram/SourceFiles/data/data_cloud_themes.cpp b/Telegram/SourceFiles/data/data_cloud_themes.cpp index 22e803c84..d6b015af8 100644 --- a/Telegram/SourceFiles/data/data_cloud_themes.cpp +++ b/Telegram/SourceFiles/data/data_cloud_themes.cpp @@ -387,26 +387,29 @@ rpl::producer<> CloudThemes::chatThemesUpdated() const { } std::optional CloudThemes::themeForEmoji( - const QString &emoji) const { - if (emoji.isEmpty()) { + const QString &emoticon) const { + const auto emoji = Ui::Emoji::Find(emoticon); + if (!emoji) { return {}; } - const auto i = ranges::find(_chatThemes, emoji, &ChatTheme::emoji); + const auto i = ranges::find(_chatThemes, emoji, [](const ChatTheme &v) { + return Ui::Emoji::Find(v.emoticon); + }); return (i != end(_chatThemes)) ? std::make_optional(*i) : std::nullopt; } rpl::producer> CloudThemes::themeForEmojiValue( - const QString &emoji) { + const QString &emoticon) { const auto testing = TestingColors(); - if (emoji.isEmpty()) { + if (!Ui::Emoji::Find(emoticon)) { return rpl::single>(std::nullopt); - } else if (auto result = themeForEmoji(emoji)) { + } else if (auto result = themeForEmoji(emoticon)) { if (testing) { return rpl::single( std::move(result) ) | rpl::then(chatThemesUpdated( ) | rpl::map([=] { - return themeForEmoji(emoji); + return themeForEmoji(emoticon); }) | rpl::filter([](const std::optional &theme) { return theme.has_value(); })); @@ -419,7 +422,7 @@ rpl::producer> CloudThemes::themeForEmojiValue( std::nullopt ) | rpl::then(chatThemesUpdated( ) | rpl::map([=] { - return themeForEmoji(emoji); + return themeForEmoji(emoticon); }) | rpl::filter([](const std::optional &theme) { return theme.has_value(); }) | rpl::take(limit)); @@ -482,12 +485,15 @@ QString CloudThemes::prepareTestingLink(const CloudTheme &theme) const { } std::optional CloudThemes::updateThemeFromLink( - const QString &emoji, + const QString &emoticon, const QMap ¶ms) { - if (!TestingColors()) { + const auto emoji = Ui::Emoji::Find(emoticon); + if (!TestingColors() || !emoji) { return std::nullopt; } - const auto i = ranges::find(_chatThemes, emoji, &ChatTheme::emoji); + const auto i = ranges::find(_chatThemes, emoji, [](const ChatTheme &v) { + return Ui::Emoji::Find(v.emoticon); + }); if (i == end(_chatThemes)) { return std::nullopt; } @@ -552,7 +558,7 @@ void CloudThemes::parseChatThemes(const QVector &list) { for (const auto &theme : list) { theme.match([&](const MTPDchatTheme &data) { _chatThemes.push_back({ - .emoji = qs(data.vemoticon()), + .emoticon = qs(data.vemoticon()), .light = CloudTheme::Parse(_session, data.vtheme(), true), .dark = CloudTheme::Parse(_session, data.vdark_theme(), true), }); diff --git a/Telegram/SourceFiles/data/data_cloud_themes.h b/Telegram/SourceFiles/data/data_cloud_themes.h index e6a1b3ca9..0976e4e90 100644 --- a/Telegram/SourceFiles/data/data_cloud_themes.h +++ b/Telegram/SourceFiles/data/data_cloud_themes.h @@ -50,7 +50,7 @@ struct CloudTheme { }; struct ChatTheme { - QString emoji; + QString emoticon; CloudTheme light; CloudTheme dark; }; @@ -71,15 +71,15 @@ public: [[nodiscard]] const std::vector &chatThemes() const; [[nodiscard]] rpl::producer<> chatThemesUpdated() const; [[nodiscard]] std::optional themeForEmoji( - const QString &emoji) const; + const QString &emoticon) const; [[nodiscard]] rpl::producer> themeForEmojiValue( - const QString &emoji); + const QString &emoticon); [[nodiscard]] static bool TestingColors(); static void SetTestingColors(bool testing); [[nodiscard]] QString prepareTestingLink(const CloudTheme &theme) const; [[nodiscard]] std::optional updateThemeFromLink( - const QString &emoji, + const QString &emoticon, const QMap ¶ms); void applyUpdate(const MTPTheme &theme); diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp index 7d9f31ea1..c0144bc4e 100644 --- a/Telegram/SourceFiles/data/data_peer.cpp +++ b/Telegram/SourceFiles/data/data_peer.cpp @@ -1004,19 +1004,24 @@ PeerId PeerData::groupCallDefaultJoinAs() const { return 0; } -void PeerData::setThemeEmoji(const QString &emoji) { - if (_themeEmoji == emoji) { +void PeerData::setThemeEmoji(const QString &emoticon) { + if (_themeEmoticon == emoticon) { return; } - _themeEmoji = emoji; - if (!emoji.isEmpty() && !owner().cloudThemes().themeForEmoji(emoji)) { + if (Ui::Emoji::Find(_themeEmoticon) == Ui::Emoji::Find(emoticon)) { + _themeEmoticon = emoticon; + return; + } + _themeEmoticon = emoticon; + if (!emoticon.isEmpty() + && !owner().cloudThemes().themeForEmoji(emoticon)) { owner().cloudThemes().refreshChatThemes(); } session().changes().peerUpdated(this, UpdateFlag::ChatThemeEmoji); } const QString &PeerData::themeEmoji() const { - return _themeEmoji; + return _themeEmoticon; } void PeerData::setIsBlocked(bool is) { diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h index a4dd2b0f1..55d940200 100644 --- a/Telegram/SourceFiles/data/data_peer.h +++ b/Telegram/SourceFiles/data/data_peer.h @@ -459,7 +459,7 @@ public: [[nodiscard]] Data::GroupCall *groupCall() const; [[nodiscard]] PeerId groupCallDefaultJoinAs() const; - void setThemeEmoji(const QString &emoji); + void setThemeEmoji(const QString &emoticon); [[nodiscard]] const QString &themeEmoji() const; const PeerId id; @@ -506,7 +506,7 @@ private: LoadedStatus _loadedStatus = LoadedStatus::Not; QString _about; - QString _themeEmoji; + QString _themeEmoticon; }; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index b12867f6f..ee0e3f4ca 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/special_buttons.h" #include "ui/emoji_config.h" #include "ui/chat/attach/attach_prepare.h" +#include "ui/chat/choose_theme_controller.h" #include "ui/widgets/buttons.h" #include "ui/widgets/inner_dropdown.h" #include "ui/widgets/dropdown_menu.h" @@ -1282,11 +1283,39 @@ void HistoryWidget::insertHashtagOrBotCommand( } } + +InlineBotQuery HistoryWidget::parseInlineBotQuery() const { + return (isChoosingTheme() || _editMsgId) + ? InlineBotQuery() + : ParseInlineBotQuery(&session(), _field); +} + +AutocompleteQuery HistoryWidget::parseMentionHashtagBotCommandQuery() const { + const auto result = (isChoosingTheme() + || (_inlineBot && !_inlineLookingUpBot)) + ? AutocompleteQuery() + : ParseMentionHashtagBotCommandQuery(_field); + if (result.query.isEmpty()) { + return result; + } else if (result.query[0] == '#' + && cRecentWriteHashtags().isEmpty() + && cRecentSearchHashtags().isEmpty()) { + session().local().readRecentHashtagsAndBots(); + } else if (result.query[0] == '@' + && cRecentInlineBots().isEmpty()) { + session().local().readRecentHashtagsAndBots(); + } else if (result.query[0] == '/' + && ((_peer->isUser() && !_peer->asUser()->isBot()) || _editMsgId)) { + return AutocompleteQuery(); + } + return result; +} + void HistoryWidget::updateInlineBotQuery() { if (!_history) { return; } - const auto query = ParseInlineBotQuery(&session(), _field); + const auto query = parseInlineBotQuery(); if (_inlineBotUsername != query.username) { _inlineBotUsername = query.username; if (_inlineBotResolveRequestId) { @@ -1369,10 +1398,11 @@ void HistoryWidget::orderWidgets() { if (_groupCallBar) { _groupCallBar->raise(); } - _topShadow->raise(); - if (_fieldAutocomplete) { - _fieldAutocomplete->raise(); + if (_chooseTheme) { + _chooseTheme->raise(); } + _topShadow->raise(); + _fieldAutocomplete->raise(); if (_membersDropdown) { _membersDropdown->raise(); } @@ -1410,6 +1440,34 @@ bool HistoryWidget::updateStickersByEmoji() { return (emoji != nullptr); } +void HistoryWidget::toggleChooseChatTheme(not_null peer) { + const auto update = [=] { + updateInlineBotQuery(); + updateControlsGeometry(); + updateControlsVisibility(); + }; + if (peer.get() != _peer) { + return; + } else if (_chooseTheme) { + if (isChoosingTheme()) { + _chooseTheme = nullptr; + update(); + } + return; + } else if (_voiceRecordBar->isActive()) { + Ui::ShowMultilineToast({ + .text = { tr::lng_chat_theme_cant_voice(tr::now) }, + }); + return; + } + _chooseTheme = std::make_unique( + this, + controller(), + peer); + _chooseTheme->shouldBeShownValue( + ) | rpl::start_with_next(update, _chooseTheme->lifetime()); +} + void HistoryWidget::fieldChanged() { const auto updateTyping = (_textUpdateEvents & TextUpdateEvent::SendTyping); @@ -1925,6 +1983,7 @@ void HistoryWidget::showHistory( _pinnedTracker = nullptr; _groupCallBar = nullptr; _groupCallTracker = nullptr; + _chooseTheme = nullptr; _membersDropdown.destroy(); _scrollToAnimation.stop(); @@ -2322,52 +2381,41 @@ void HistoryWidget::updateControlsVisibility() { if (_contactStatus) { _contactStatus->show(); } - if (!editingMessage() && (isBlocked() || isJoinChannel() || isMuteUnmute() || isBotStart() || isReportMessages())) { - if (isReportMessages()) { - _unblock->hide(); - _joinChannel->hide(); - _muteUnmute->hide(); - _botStart->hide(); - if (_reportMessages->isHidden()) { - _reportMessages->clearState(); - _reportMessages->show(); - } + if (isChoosingTheme() + || (!editingMessage() + && (isBlocked() + || isJoinChannel() + || isMuteUnmute() + || isBotStart() + || isReportMessages()))) { + const auto toggle = [&](Ui::FlatButton *shown) { + const auto toggleOne = [&](not_null button) { + if (button.get() != shown) { + button->hide(); + } else if (button->isHidden()) { + button->clearState(); + button->show(); + } + }; + toggleOne(_reportMessages); + toggleOne(_joinChannel); + toggleOne(_muteUnmute); + toggleOne(_botStart); + toggleOne(_unblock); + }; + if (isChoosingTheme()) { + toggle(nullptr); + _chooseTheme->show(); + } else if (isReportMessages()) { + toggle(_reportMessages); } else if (isBlocked()) { - _reportMessages->hide(); - _joinChannel->hide(); - _muteUnmute->hide(); - _botStart->hide(); - if (_unblock->isHidden()) { - _unblock->clearState(); - _unblock->show(); - } + toggle(_unblock); } else if (isJoinChannel()) { - _reportMessages->hide(); - _unblock->hide(); - _muteUnmute->hide(); - _botStart->hide(); - if (_joinChannel->isHidden()) { - _joinChannel->clearState(); - _joinChannel->show(); - } + toggle(_joinChannel); } else if (isMuteUnmute()) { - _reportMessages->hide(); - _unblock->hide(); - _joinChannel->hide(); - _botStart->hide(); - if (_muteUnmute->isHidden()) { - _muteUnmute->clearState(); - _muteUnmute->show(); - } + toggle(_muteUnmute); } else if (isBotStart()) { - _reportMessages->hide(); - _unblock->hide(); - _joinChannel->hide(); - _muteUnmute->hide(); - if (_botStart->isHidden()) { - _botStart->clearState(); - _botStart->show(); - } + toggle(_botStart); } _kbShown = false; _fieldAutocomplete->hide(); @@ -3289,6 +3337,9 @@ void HistoryWidget::hideChildWidgets() { if (_voiceRecordBar) { _voiceRecordBar->hideFast(); } + if (_chooseTheme) { + _chooseTheme->hide(); + } hideChildren(); } @@ -3908,7 +3959,7 @@ void HistoryWidget::inlineBotResolveDone( }(); session().data().processChats(data.vchats()); - const auto query = ParseInlineBotQuery(&session(), _field); + const auto query = parseInlineBotQuery(); if (_inlineBotUsername == query.username) { applyInlineBotQuery( query.lookingUpBot ? resolvedBot : query.bot, @@ -3953,6 +4004,10 @@ bool HistoryWidget::isJoinChannel() const { return _peer && _peer->isChannel() && !_peer->asChannel()->amIn(); } +bool HistoryWidget::isChoosingTheme() const { + return _chooseTheme && _chooseTheme->shouldBeShown(); +} + bool HistoryWidget::isMuteUnmute() const { return _peer && ((_peer->isBroadcast() && !_peer->asChannel()->canPublish()) @@ -4337,24 +4392,7 @@ void HistoryWidget::checkFieldAutocomplete() { return; } - const auto isInlineBot = _inlineBot && !_inlineLookingUpBot; - const auto autocomplete = isInlineBot - ? AutocompleteQuery() - : ParseMentionHashtagBotCommandQuery(_field); - if (!autocomplete.query.isEmpty()) { - if (autocomplete.query[0] == '#' - && cRecentWriteHashtags().isEmpty() - && cRecentSearchHashtags().isEmpty()) { - session().local().readRecentHashtagsAndBots(); - } else if (autocomplete.query[0] == '@' - && cRecentInlineBots().isEmpty()) { - session().local().readRecentHashtagsAndBots(); - } else if (autocomplete.query[0] == '/' - && ((_peer->isUser() && !_peer->asUser()->isBot()) - || _editMsgId)) { - return; - } - } + const auto autocomplete = parseMentionHashtagBotCommandQuery(); _fieldAutocomplete->showFiltered( _peer, autocomplete.query, @@ -4922,7 +4960,14 @@ void HistoryWidget::updateHistoryGeometry( if (_contactStatus) { newScrollHeight -= _contactStatus->height(); } - if (!editingMessage() && (isBlocked() || isBotStart() || isJoinChannel() || isMuteUnmute() || isReportMessages())) { + if (isChoosingTheme()) { + newScrollHeight -= _chooseTheme->height(); + } else if (!editingMessage() + && (isBlocked() + || isBotStart() + || isJoinChannel() + || isMuteUnmute() + || isReportMessages())) { newScrollHeight -= _unblock->height(); } else { if (editingMessage() || _canSendMessages) { @@ -6136,16 +6181,17 @@ void HistoryWidget::editMessage(FullMsgId itemId) { } void HistoryWidget::editMessage(not_null item) { - if (_voiceRecordBar->isActive()) { - controller()->show( - Box(tr::lng_edit_caption_voice(tr::now))); - return; - } if (const auto media = item->media()) { if (media->allowsEditCaption()) { controller()->show(Box(controller(), item)); return; } + } else if (_chooseTheme) { + toggleChooseChatTheme(_peer); + } else if (_voiceRecordBar->isActive()) { + controller()->show( + Box(tr::lng_edit_caption_voice(tr::now))); + return; } if (isRecording()) { diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index dd69a77fa..8bd0c68ea 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -25,6 +25,8 @@ struct FileLoadResult; struct SendingAlbum; enum class SendMediaType; class MessageLinksParser; +struct InlineBotQuery; +struct AutocompleteQuery; namespace MTP { class Error; @@ -77,6 +79,7 @@ enum class ReportReason; namespace Toast { class Instance; } // namespace Toast +class ChooseThemeController; } // namespace Ui namespace Window { @@ -237,6 +240,8 @@ public: void clearDelayedShowAt(); void saveFieldToHistoryLocalDraft(); + void toggleChooseChatTheme(not_null peer); + void applyCloudDraft(History *history); void updateHistoryDownPosition(); @@ -454,6 +459,10 @@ private: std::optional writeRestriction() const; void orderWidgets(); + [[nodiscard]] InlineBotQuery parseInlineBotQuery() const; + [[nodiscard]] auto parseMentionHashtagBotCommandQuery() const + -> AutocompleteQuery; + void clearInlineBot(); void inlineBotChanged(); @@ -585,19 +594,21 @@ private: void inlineBotResolveDone(const MTPcontacts_ResolvedPeer &result); void inlineBotResolveFail(const MTP::Error &error, const QString &username); - bool isRecording() const; + [[nodiscard]] bool isRecording() const; - bool isBotStart() const; - bool isBlocked() const; - bool isJoinChannel() const; - bool isMuteUnmute() const; - bool isReportMessages() const; + [[nodiscard]] bool isBotStart() const; + [[nodiscard]] bool isBlocked() const; + [[nodiscard]] bool isJoinChannel() const; + [[nodiscard]] bool isMuteUnmute() const; + [[nodiscard]] bool isReportMessages() const; bool updateCmdStartShown(); void updateSendButtonType(); - bool showRecordButton() const; - bool showInlineBotCancel() const; + [[nodiscard]] bool showRecordButton() const; + [[nodiscard]] bool showInlineBotCancel() const; void refreshSilentToggle(); + [[nodiscard]] bool isChoosingTheme() const; + void setupScheduledToggle(); void refreshScheduledToggle(); @@ -689,7 +700,7 @@ private: bool _unreadMentionsIsShown = false; object_ptr _unreadMentions; - object_ptr _fieldAutocomplete; + const object_ptr _fieldAutocomplete; object_ptr _supportAutocomplete; std::unique_ptr _fieldLinksParser; @@ -726,6 +737,8 @@ private: object_ptr _kbScroll; const not_null _keyboard; + std::unique_ptr _chooseTheme; + object_ptr _membersDropdown = { nullptr }; base::Timer _membersDropdownShowTimer; diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 2205b98ff..bb1e5d76f 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -1353,6 +1353,10 @@ void MainWidget::clearChooseReportMessages() { _history->setChooseReportMessagesDetails({}, nullptr); } +void MainWidget::toggleChooseChatTheme(not_null peer) { + _history->toggleChooseChatTheme(peer); +} + void MainWidget::ui_showPeerHistory( PeerId peerId, const SectionShow ¶ms, diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h index 7266b413e..2aa0e42ef 100644 --- a/Telegram/SourceFiles/mainwidget.h +++ b/Telegram/SourceFiles/mainwidget.h @@ -213,6 +213,8 @@ public: Fn done); void clearChooseReportMessages(); + void toggleChooseChatTheme(not_null peer); + void ui_showPeerHistory( PeerId peer, const SectionShow ¶ms, diff --git a/Telegram/SourceFiles/ui/chat/chat_theme.cpp b/Telegram/SourceFiles/ui/chat/chat_theme.cpp index 61c898de0..ffdf8a8ac 100644 --- a/Telegram/SourceFiles/ui/chat/chat_theme.cpp +++ b/Telegram/SourceFiles/ui/chat/chat_theme.cpp @@ -511,6 +511,11 @@ const BackgroundState &ChatTheme::backgroundState(QSize area) { return _backgroundState; } +void ChatTheme::clearBackgroundState() { + _backgroundState = BackgroundState(); + _backgroundFade.stop(); +} + bool ChatTheme::readyForBackgroundRotation() const { Expects(_cacheBackgroundTimer.has_value()); diff --git a/Telegram/SourceFiles/ui/chat/chat_theme.h b/Telegram/SourceFiles/ui/chat/chat_theme.h index 75bb4c787..bcc357fbd 100644 --- a/Telegram/SourceFiles/ui/chat/chat_theme.h +++ b/Telegram/SourceFiles/ui/chat/chat_theme.h @@ -137,6 +137,7 @@ public: QRect viewport, QRect clip); [[nodiscard]] const BackgroundState &backgroundState(QSize area); + void clearBackgroundState(); [[nodiscard]] rpl::producer<> repaintBackgroundRequests() const; void rotateComplexGradientBackground(); @@ -157,7 +158,6 @@ private: void cacheBubblesNow(); void cacheBubblesAsync( const CacheBackgroundRequest &request); - void setCachedBubbles(CacheBackgroundResult &&cached); [[nodiscard]] CacheBackgroundRequest cacheBubblesRequest( QSize area) const; diff --git a/Telegram/SourceFiles/ui/chat/choose_theme_controller.cpp b/Telegram/SourceFiles/ui/chat/choose_theme_controller.cpp new file mode 100644 index 000000000..15742b7e0 --- /dev/null +++ b/Telegram/SourceFiles/ui/chat/choose_theme_controller.cpp @@ -0,0 +1,424 @@ +/* +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 "ui/chat/choose_theme_controller.h" + +#include "ui/rp_widget.h" +#include "ui/widgets/shadow.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/buttons.h" +#include "ui/chat/chat_theme.h" +#include "ui/wrap/vertical_layout.h" +#include "main/main_session.h" +#include "window/window_session_controller.h" +#include "window/themes/window_theme.h" +#include "data/data_session.h" +#include "data/data_peer.h" +#include "data/data_cloud_themes.h" +#include "data/data_document.h" +#include "data/data_document_media.h" +#include "lang/lang_keys.h" +#include "apiwrap.h" +#include "styles/style_widgets.h" +#include "styles/style_layers.h" // boxTitle. +#include "styles/style_settings.h" + +namespace Ui { +namespace { + +constexpr auto kDisableElement = "disable"_cs; + +[[nodiscard]] QImage GeneratePreview(not_null theme) { + const auto &colors = theme->background().colors; + auto result = Images::GenerateGradient( + st::settingsThemePreviewSize * style::DevicePixelRatio(), + colors.empty() ? std::vector{ 1, QColor(0, 0, 0) } : colors + ).convertToFormat(QImage::Format_ARGB32_Premultiplied); + Images::prepareRound(result, ImageRoundRadius::Large); + return result; +} + +[[nodiscard]] QImage GenerateEmptyPreview() { + auto result = QImage( + st::settingsThemePreviewSize * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + result.fill(st::settingsThemeNotSupportedBg->c); + Images::prepareRound(result, ImageRoundRadius::Large); + return result; +} + +} // namespace + +struct ChooseThemeController::Entry { + uint64 id = 0; + std::shared_ptr theme; + std::shared_ptr media; + QImage preview; + EmojiPtr emoji = nullptr; + QRect geometry; +}; + +ChooseThemeController::ChooseThemeController( + not_null parent, + not_null window, + not_null peer) +: _controller(window) +, _peer(peer) +, _wrap(std::make_unique(parent)) +, _topShadow(std::make_unique(parent)) +, _content(_wrap->add(object_ptr(_wrap.get()))) +, _inner(CreateChild(_content.get())) +, _dark(Window::Theme::IsThemeDarkValue()) { + init(parent->sizeValue()); +} + +ChooseThemeController::~ChooseThemeController() { + _controller->clearPeerThemeOverride(_peer); +} + +void ChooseThemeController::init(rpl::producer outer) { + using namespace rpl::mappers; + + const auto themes = &_controller->session().data().cloudThemes(); + const auto &list = themes->chatThemes(); + if (!list.empty()) { + fill(list); + } else { + themes->refreshChatThemes(); + themes->chatThemesUpdated( + ) | rpl::take(1) | rpl::start_with_next([=] { + fill(themes->chatThemes()); + }, lifetime()); + } + + const auto skip = st::normalFont->spacew * 2; + _wrap->insert( + 0, + object_ptr( + _wrap.get(), + tr::lng_chat_theme_title(), + st::boxTitle), + style::margins{ skip * 2, skip, skip * 2, skip }); + _wrap->paintRequest( + ) | rpl::start_with_next([=](QRect clip) { + QPainter(_wrap.get()).fillRect(clip, st::windowBg); + }, lifetime()); + + initButtons(); + initList(); + + std::move( + outer + ) | rpl::start_with_next([=](QSize outer) { + _wrap->resizeToWidth(outer.width()); + _wrap->move(0, outer.height() - _wrap->height()); + const auto line = st::lineWidth; + _topShadow->setGeometry(0, _wrap->y() - line, outer.width(), line); + }, lifetime()); + + rpl::combine( + _shouldBeShown.value(), + _forceHidden.value(), + _1 && !_2 + ) | rpl::start_with_next([=](bool shown) { + _wrap->setVisible(shown); + _topShadow->setVisible(shown); + }, lifetime()); +} + +void ChooseThemeController::initButtons() { + const auto controls = _wrap->add(object_ptr(_wrap.get())); + const auto cancel = CreateChild( + controls, + tr::lng_cancel(), + st::defaultLightButton); + const auto apply = CreateChild( + controls, + tr::lng_chat_theme_apply(), + st::defaultActiveButton); + const auto skip = st::normalFont->spacew * 2; + controls->resize( + skip + cancel->width() + skip + apply->width() + skip, + skip + apply->height() + skip); + rpl::combine( + controls->widthValue(), + cancel->widthValue(), + apply->widthValue() + ) | rpl::start_with_next([=]( + int outer, + int cancelWidth, + int applyWidth) { + const auto inner = skip + cancelWidth + skip + applyWidth + skip; + const auto left = (outer - inner) / 2; + cancel->moveToLeft(left, skip); + apply->moveToRight(left, skip); + }, controls->lifetime()); + + const auto findChosen = [=]() -> const Entry* { + if (_chosen.isEmpty()) { + return nullptr; + } + for (const auto &entry : _entries) { + if (!entry.id && _chosen == kDisableElement.utf16()) { + return &entry; + } else if (_chosen == entry.emoji->text()) { + return &entry; + } + } + return nullptr; + }; + const auto changed = [=] { + if (_chosen.isEmpty()) { + return false; + } + const auto now = Ui::Emoji::Find(_peer->themeEmoji()); + if (_chosen == kDisableElement.utf16()) { + return !now; + } + for (const auto &entry : _entries) { + if (entry.id && entry.emoji->text() == _chosen) { + return (now != entry.emoji); + } + } + return false; + }; + cancel->setClickedCallback([=] { + if (const auto chosen = findChosen()) { + if (Ui::Emoji::Find(_peer->themeEmoji()) != chosen->emoji) { + clearCurrentBackgroundState(); + } + } + _controller->toggleChooseChatTheme(_peer); + }); + apply->setClickedCallback([=] { + if (const auto chosen = findChosen()) { + if (Ui::Emoji::Find(_peer->themeEmoji()) != chosen->emoji) { + const auto now = chosen->id ? _chosen : QString(); + _peer->setThemeEmoji(now); + if (chosen->theme) { + // Remember while changes propagate through event loop. + _controller->pushLastUsedChatTheme(chosen->theme); + } + const auto api = &_peer->session().api(); + api->request(MTPmessages_SetChatTheme( + _peer->input, + MTP_string(now) + )).done([=](const MTPUpdates &result) { + api->applyUpdates(result); + }).send(); + } + } + _controller->toggleChooseChatTheme(_peer); + }); +} + +void ChooseThemeController::paintEntry(QPainter &p, const Entry &entry) { + const auto geometry = entry.geometry; + p.drawImage(geometry, entry.preview); + + if (entry.theme) { + const auto received = QRect( + st::settingsThemeBubblePosition, + st::settingsThemeBubbleSize); + const auto sent = QRect( + (geometry.width() + - received.width() + - st::settingsThemeBubblePosition.x()), + received.y() + received.height() + st::settingsThemeBubbleSkip, + received.width(), + received.height()); + const auto radius = st::settingsThemeBubbleRadius; + + const auto sentBg = entry.theme->palette()->msgOutBg()->c; + const auto receivedBg = entry.theme->palette()->msgInBg()->c; + + PainterHighQualityEnabler hq(p); + p.setPen(Qt::NoPen); + + p.setBrush(receivedBg); + p.drawRoundedRect(received.translated(geometry.topLeft()), radius, radius); + p.setBrush(sentBg); + p.drawRoundedRect(sent.translated(geometry.topLeft()), radius, radius); + } + const auto size = Ui::Emoji::GetSizeLarge(); + const auto factor = style::DevicePixelRatio(); + const auto skip = st::normalFont->spacew * 2; + Ui::Emoji::Draw( + p, + entry.emoji, + size, + (geometry.x() + + (geometry.width() - (size / factor)) / 2), + (geometry.y() + geometry.height() - (size / factor) - skip)); + +} + +void ChooseThemeController::initList() { + _content->resize( + _content->width(), + st::settingsThemePreviewSize.height()); + _inner->setMouseTracking(true); + + _inner->paintRequest( + ) | rpl::start_with_next([=](QRect clip) { + auto p = QPainter(_inner.get()); + for (const auto &entry : _entries) { + if (entry.preview.isNull() || !clip.intersects(entry.geometry)) { + continue; + } + paintEntry(p, entry); + } + }, lifetime()); + const auto byPoint = [=](QPoint position) -> const Entry* { + for (const auto &entry : _entries) { + if (entry.geometry.contains(position)) { + return &entry; + } + } + return nullptr; + }; + const auto chosenText = [=](const Entry *entry) { + if (!entry) { + return QString(); + } else if (entry->id) { + return entry->emoji->text(); + } else { + return kDisableElement.utf16(); + } + }; + _inner->events( + ) | rpl::start_with_next([=](not_null event) { + const auto type = event->type(); + if (type == QEvent::MouseMove) { + const auto mouse = static_cast(event.get()); + _inner->setCursor(byPoint(mouse->pos()) + ? style::cur_pointer + : style::cur_default); + } else if (type == QEvent::MouseButtonPress) { + const auto mouse = static_cast(event.get()); + _pressed = chosenText(byPoint(mouse->pos())); + } else if (type == QEvent::MouseButtonRelease) { + const auto mouse = static_cast(event.get()); + const auto entry = byPoint(mouse->pos()); + const auto chosen = chosenText(entry); + if (entry && chosen == _pressed && chosen != _chosen) { + clearCurrentBackgroundState(); + _chosen = chosen; + if (entry->theme || !entry->id) { + _controller->overridePeerTheme(_peer, entry->theme); + } + } + _pressed = QString(); + } + }, lifetime()); +} + +void ChooseThemeController::clearCurrentBackgroundState() { + for (const auto &entry : _entries) { + if (entry.theme && entry.emoji && entry.emoji->text() == _chosen) { + entry.theme->clearBackgroundState(); + } + } +} + +void ChooseThemeController::fill( + const std::vector &themes) { + if (themes.empty()) { + return; + } + const auto count = int(themes.size()) + 1; + const auto single = st::settingsThemePreviewSize; + const auto skip = st::normalFont->spacew * 2; + const auto full = single.width() * count + skip * (count + 1); + _inner->resize(full, single.height()); + + _dark.value( + ) | rpl::start_with_next([=](bool dark) { + clearCurrentBackgroundState(); + + _cachingLifetime.destroy(); + const auto old = base::take(_entries); + auto x = skip; + _entries.push_back({ + .preview = GenerateEmptyPreview(), + .emoji = Ui::Emoji::Find(QString::fromUtf8("\xe2\x9d\x8c")), + .geometry = QRect(QPoint(x, 0), single), + }); + Assert(_entries.front().emoji != nullptr); + style::PaletteChanged( + ) | rpl::start_with_next([=] { + _entries.front().preview = GenerateEmptyPreview(); + }, _cachingLifetime); + + x += single.width() + skip; + for (const auto &theme : themes) { + const auto emoji = Ui::Emoji::Find(theme.emoticon); + if (!emoji) { + continue; + } + const auto &used = dark ? theme.dark : theme.light; + const auto id = used.id; + _entries.push_back({ + .id = id, + .emoji = emoji, + .geometry = QRect(QPoint(x, 0), single), + }); + _controller->cachedChatThemeValue( + used + ) | rpl::filter([=](const std::shared_ptr &data) { + return data && (data->key() == id); + }) | rpl::take( + 1 + ) | rpl::start_with_next([=](std::shared_ptr &&data) { + const auto id = data->key(); + const auto i = ranges::find(_entries, id, &Entry::id); + if (i != end(_entries)) { + i->theme = std::move(data); + i->preview = GeneratePreview(i->theme.get()); + if (_chosen == i->emoji->text()) { + _controller->overridePeerTheme(_peer, i->theme); + } + _inner->update(); + } + }, _cachingLifetime); + _entries.back().preview; + x += single.width() + skip; + } + }, lifetime()); + _shouldBeShown = true; +} + +bool ChooseThemeController::shouldBeShown() const { + return _shouldBeShown.current(); +} + +rpl::producer ChooseThemeController::shouldBeShownValue() const { + return _shouldBeShown.value(); +} + +int ChooseThemeController::height() const { + return shouldBeShown() ? _wrap->height() : 0; +} + +void ChooseThemeController::hide() { + _forceHidden = true; +} + +void ChooseThemeController::show() { + _forceHidden = false; +} + +void ChooseThemeController::raise() { + _wrap->raise(); + _topShadow->raise(); +} + +rpl::lifetime &ChooseThemeController::lifetime() { + return _wrap->lifetime(); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/chat/choose_theme_controller.h b/Telegram/SourceFiles/ui/chat/choose_theme_controller.h new file mode 100644 index 000000000..daec775a7 --- /dev/null +++ b/Telegram/SourceFiles/ui/chat/choose_theme_controller.h @@ -0,0 +1,73 @@ +/* +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 + +class PeerData; + +namespace Window { +class SessionController; +} // namespace Window + +namespace Data { +struct ChatTheme; +} // namespace Data + +namespace Ui { + +class RpWidget; +class PlainShadow; +class VerticalLayout; + +class ChooseThemeController final { +public: + ChooseThemeController( + not_null parent, + not_null window, + not_null peer); + ~ChooseThemeController(); + + [[nodiscard]] bool shouldBeShown() const; + [[nodiscard]] rpl::producer shouldBeShownValue() const; + [[nodiscard]] int height() const; + + void hide(); + void show(); + void raise(); + + [[nodiscard]] rpl::lifetime &lifetime(); + +private: + struct Entry; + + void init(rpl::producer outer); + void initButtons(); + void initList(); + void fill(const std::vector &themes); + + void clearCurrentBackgroundState(); + void paintEntry(QPainter &p, const Entry &entry); + + const not_null _controller; + const not_null _peer; + const std::unique_ptr _wrap; + const std::unique_ptr _topShadow; + + const not_null _content; + const not_null _inner; + std::vector _entries; + QString _pressed; + QString _chosen; + + rpl::variable _shouldBeShown = false; + rpl::variable _forceHidden = false; + rpl::variable _dark = false; + rpl::lifetime _cachingLifetime; + +}; + +} // namespace Ui diff --git a/Telegram/SourceFiles/window/section_widget.cpp b/Telegram/SourceFiles/window/section_widget.cpp index 02801126d..f86a8c879 100644 --- a/Telegram/SourceFiles/window/section_widget.cpp +++ b/Telegram/SourceFiles/window/section_widget.cpp @@ -25,8 +25,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Window { namespace { -constexpr auto kDarkValueThreshold = 0.5; - [[nodiscard]] rpl::producer PeerThemeEmojiValue( not_null peer) { return peer->session().changes().peerFlagsValue( @@ -51,17 +49,9 @@ constexpr auto kDarkValueThreshold = 0.5; [[nodiscard]] auto MaybeCloudThemeValueFromPeer( not_null peer) -> rpl::producer> { - auto isThemeDarkValue = rpl::single( - rpl::empty_value() - ) | rpl::then( - style::PaletteChanged() - ) | rpl::map([] { - return (st::dialogsBg->c.valueF() < kDarkValueThreshold); - }) | rpl::distinct_until_changed(); - return rpl::combine( MaybeChatThemeDataValueFromPeer(peer), - std::move(isThemeDarkValue) + Theme::IsThemeDarkValue() | rpl::distinct_until_changed() ) | rpl::map([](std::optional theme, bool night) { return !theme ? std::nullopt @@ -304,7 +294,7 @@ auto ChatThemeValueFromPeer( not_null controller, not_null peer) -> rpl::producer> { - return MaybeCloudThemeValueFromPeer( + auto cloud = MaybeCloudThemeValueFromPeer( peer ) | rpl::map([=](std::optional theme) -> rpl::producer> { @@ -314,6 +304,17 @@ auto ChatThemeValueFromPeer( return controller->cachedChatThemeValue(*theme); }) | rpl::flatten_latest( ) | rpl::distinct_until_changed(); + + return rpl::combine( + std::move(cloud), + controller->peerThemeOverrideValue() + ) | rpl::map([=]( + std::shared_ptr &&cloud, + PeerThemeOverride &&overriden) { + return (overriden.peer == peer.get()) + ? std::move(overriden.theme) + : std::move(cloud); + }); } } // namespace Window diff --git a/Telegram/SourceFiles/window/themes/window_theme.cpp b/Telegram/SourceFiles/window/themes/window_theme.cpp index fbed3c8ec..affb0a03b 100644 --- a/Telegram/SourceFiles/window/themes/window_theme.cpp +++ b/Telegram/SourceFiles/window/themes/window_theme.cpp @@ -46,6 +46,7 @@ namespace { constexpr auto kThemeFileSizeLimit = 5 * 1024 * 1024; constexpr auto kBackgroundSizeLimit = 25 * 1024 * 1024; constexpr auto kNightThemeFile = ":/gui/night.tdesktop-theme"_cs; +constexpr auto kDarkValueThreshold = 0.5; struct Applying { Saved data; @@ -1450,6 +1451,16 @@ bool LoadFromContent( out); } +rpl::producer IsThemeDarkValue() { + return rpl::single( + rpl::empty_value() + ) | rpl::then( + style::PaletteChanged() + ) | rpl::map([] { + return (st::dialogsBg->c.valueF() < kDarkValueThreshold); + }); +} + QString EditingPalettePath() { return cWorkingDir() + "tdata/editing-theme.tdesktop-palette"; } diff --git a/Telegram/SourceFiles/window/themes/window_theme.h b/Telegram/SourceFiles/window/themes/window_theme.h index 02cd7ee0e..22cd32f97 100644 --- a/Telegram/SourceFiles/window/themes/window_theme.h +++ b/Telegram/SourceFiles/window/themes/window_theme.h @@ -97,6 +97,8 @@ void ResetToSomeDefault(); [[nodiscard]] bool IsNonDefaultBackground(); void Revert(); +[[nodiscard]] rpl::producer IsThemeDarkValue(); + [[nodiscard]] QString EditingPalettePath(); // NB! This method looks to Core::App().settings() to get colorizer by 'file'. diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 7dab65674..9182a4442 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -487,6 +487,11 @@ void Filler::addUserActions(not_null user) { [=] { AddBotToGroup::Start(user); }); } addPollAction(user); + if (!user->isBot()) { + _addAction( + tr::lng_chat_theme_change(tr::now), + [=] { controller->toggleChooseChatTheme(user); }); + } if (user->canExportChatHistory()) { _addAction( tr::lng_profile_export_chat(tr::now), diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 839ed2e12..e676c3303 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -125,6 +125,14 @@ void ActivateWindow(not_null controller) { Ui::ActivateWindowDelayed(window); } +bool operator==(const PeerThemeOverride &a, const PeerThemeOverride &b) { + return (a.peer == b.peer) && (a.theme == b.theme); +} + +bool operator!=(const PeerThemeOverride &a, const PeerThemeOverride &b) { + return !(a == b); +} + DateClickHandler::DateClickHandler(Dialogs::Key chat, QDate date) : _chat(chat) , _date(date) { @@ -1208,6 +1216,10 @@ void SessionController::clearChooseReportMessages() { content()->clearChooseReportMessages(); } +void SessionController::toggleChooseChatTheme(not_null peer) { + content()->toggleChooseChatTheme(peer); +} + void SessionController::updateColumnLayout() { content()->updateColumnLayout(); } @@ -1397,7 +1409,7 @@ auto SessionController::cachedChatThemeValue( const auto i = _customChatThemes.find(key); if (i != end(_customChatThemes)) { if (auto strong = i->second.theme.lock()) { - pushToLastUsed(strong); + pushLastUsedChatTheme(strong); return rpl::single(std::move(strong)); } } @@ -1413,12 +1425,12 @@ auto SessionController::cachedChatThemeValue( if (theme->key() != key) { return false; } - pushToLastUsed(theme); + pushLastUsedChatTheme(theme); return true; }) | rpl::take(limit)); } -void SessionController::pushToLastUsed( +void SessionController::pushLastUsedChatTheme( const std::shared_ptr &theme) { const auto i = ranges::find(_lastUsedCustomChatThemes, theme); if (i == end(_lastUsedCustomChatThemes)) { @@ -1444,6 +1456,21 @@ void SessionController::clearCachedChatThemes() { _customChatThemes.clear(); } +void SessionController::overridePeerTheme( + not_null peer, + std::shared_ptr theme) { + _peerThemeOverride = PeerThemeOverride{ + peer, + theme ? theme : _defaultChatTheme, + }; +} + +void SessionController::clearPeerThemeOverride(not_null peer) { + if (_peerThemeOverride.current().peer == peer.get()) { + _peerThemeOverride = PeerThemeOverride(); + } +} + void SessionController::pushDefaultChatBackground() { const auto background = Theme::Background(); const auto &paper = background->paper(); diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 81d6697d6..c524f1bc2 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -75,6 +75,13 @@ enum class GifPauseReason { using GifPauseReasons = base::flags; inline constexpr bool is_flag_type(GifPauseReason) { return true; }; +struct PeerThemeOverride { + PeerData *peer = nullptr; + std::shared_ptr theme; +}; +bool operator==(const PeerThemeOverride &a, const PeerThemeOverride &b); +bool operator!=(const PeerThemeOverride &a, const PeerThemeOverride &b); + class DateClickHandler : public ClickHandler { public: DateClickHandler(Dialogs::Key chat, QDate date); @@ -378,6 +385,8 @@ public: Fn done); void clearChooseReportMessages(); + void toggleChooseChatTheme(not_null peer); + base::Variable &dialogsListFocused() { return _dialogsListFocused; } @@ -412,6 +421,16 @@ public: -> rpl::producer>; void setChatStyleTheme(const std::shared_ptr &theme); void clearCachedChatThemes(); + void pushLastUsedChatTheme(const std::shared_ptr &theme); + + void overridePeerTheme( + not_null peer, + std::shared_ptr theme); + void clearPeerThemeOverride(not_null peer); + [[nodiscard]] auto peerThemeOverrideValue() const + -> rpl::producer { + return _peerThemeOverride.value(); + } struct PaintContextArgs { not_null theme; @@ -464,7 +483,6 @@ private: [[nodiscard]] Ui::ChatThemeBackgroundData backgroundData( CachedTheme &theme, bool generateGradient = true) const; - void pushToLastUsed(const std::shared_ptr &theme); const not_null _window; const std::unique_ptr _emojiInteractions; @@ -500,6 +518,7 @@ private: const std::unique_ptr _chatStyle; std::weak_ptr _chatStyleTheme; std::deque> _lastUsedCustomChatThemes; + rpl::variable _peerThemeOverride; rpl::lifetime _lifetime;