diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 5d367d577..21427c7c9 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -180,6 +180,8 @@ PRIVATE boxes/peers/edit_peer_history_visibility_box.h boxes/peers/edit_peer_permissions_box.cpp boxes/peers/edit_peer_permissions_box.h + boxes/peers/edit_peer_reactions.cpp + boxes/peers/edit_peer_reactions.h boxes/peers/edit_peer_requests_box.cpp boxes/peers/edit_peer_requests_box.h boxes/peers/edit_peer_type_box.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index e3da4ba3f..f5e1a0ad4 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1005,9 +1005,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_manage_peer_removed_users" = "Removed users"; "lng_manage_peer_permissions" = "Permissions"; "lng_manage_peer_invite_links" = "Invite links"; +"lng_manage_peer_reactions" = "Reactions"; +"lng_manage_peer_reactions_off" = "Off"; "lng_manage_peer_requests" = "Member Requests"; "lng_manage_peer_requests_channel" = "Subscriber Requests"; +"lng_manage_peer_reactions_enable" = "Enable Reactions"; +"lng_manage_peer_reactions_about" = "Allow members to react to group messages."; +"lng_manage_peer_reactions_about_channel" = "Allow subscribers to react to channel posts."; +"lng_manage_peer_reactions_available" = "Available reactions"; + "lng_manage_peer_group_type" = "Group type"; "lng_manage_peer_channel_type" = "Channel type"; "lng_manage_peer_link_type" = "Link type"; diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp index 859504da4..922ead843 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_info_box.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/edit_peer_invite_links.h" #include "boxes/peers/edit_linked_chat_box.h" #include "boxes/peers/edit_peer_requests_box.h" +#include "boxes/peers/edit_peer_reactions.h" #include "boxes/stickers_box.h" #include "ui/boxes/single_choice_box.h" #include "chat_helpers/emoji_suggestions_widget.h" @@ -31,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_peer.h" #include "data/data_session.h" #include "data/data_changes.h" +#include "data/data_message_reactions.h" #include "history/admin_log/history_admin_log_section.h" #include "info/profile/info_profile_values.h" #include "lang/lang_keys.h" @@ -288,7 +290,8 @@ private: object_ptr createManageGroupButtons(); object_ptr createStickersEdit(); - bool canEditInformation() const; + [[nodiscard]] bool canEditInformation() const; + [[nodiscard]] bool canEditReactions() const; void refreshHistoryVisibility(); void showEditPeerTypeBox( std::optional> error = {}); @@ -596,6 +599,17 @@ bool Controller::canEditInformation() const { return false; } +bool Controller::canEditReactions() const { + if (const auto channel = _peer->asChannel()) { + return channel->amCreator() + || (channel->adminRights() & ChatAdminRight::ChangeInfo); + } else if (const auto chat = _peer->asChat()) { + return chat->amCreator() + || (chat->adminRights() & ChatAdminRight::ChangeInfo); + } + return false; +} + void Controller::refreshHistoryVisibility() { if (!_controls.historyVisibilityWrap) { return; @@ -1017,6 +1031,39 @@ void Controller::fillManageSection() { }, wrap->lifetime()); } } + if (canEditReactions()) { + const auto session = &_peer->session(); + auto reactionsCount = Info::Profile::MigratedOrMeValue( + _peer + ) | rpl::map( + Info::Profile::AllowedReactionsCountValue + ) | rpl::flatten_latest(); + auto fullCount = Info::Profile::FullReactionsCountValue(session); + auto label = rpl::combine( + std::move(reactionsCount), + std::move(fullCount) + ) | rpl::map([=](int allowed, int total) { + return allowed + ? QString::number(allowed) + " / " + QString::number(total) + : tr::lng_manage_peer_reactions_off(tr::now); + }); + const auto done = [=](const std::vector &chosen) { + SaveAllowedReactions(_peer, chosen); + }; + AddButtonWithCount( + _controls.buttonsLayout, + tr::lng_manage_peer_reactions(), + std::move(label), + [=] { + _navigation->parentController()->show(Box( + EditAllowedReactionsBox, + !_peer->isBroadcast(), + session->data().reactions().list(), + session->data().reactions().list(_peer), + done)); + }, + st::infoIconReactions); + } if (canViewAdmins) { AddButtonWithCount( _controls.buttonsLayout, diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp new file mode 100644 index 000000000..0df4cc658 --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.cpp @@ -0,0 +1,209 @@ +/* +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/peers/edit_peer_reactions.h" + +#include "data/data_message_reactions.h" +#include "data/data_document.h" +#include "data/data_document_media.h" +#include "data/data_peer.h" +#include "data/data_chat.h" +#include "data/data_channel.h" +#include "data/data_session.h" +#include "main/main_session.h" +#include "apiwrap.h" +#include "lang/lang_keys.h" +#include "ui/widgets/buttons.h" +#include "info/profile/info_profile_icon.h" +#include "settings/settings_common.h" +#include "styles/style_settings.h" +#include "styles/style_info.h" + +namespace { + +using Data::Reaction; + +void AddReactionIcon( + not_null button, + not_null document) { + struct State { + std::shared_ptr media; + QImage image; + }; + + const auto size = st::editPeerReactionsPreview; + const auto state = button->lifetime().make_state(State{ + .media = document->createMediaView(), + }); + const auto icon = Ui::CreateChild(button.get()); + icon->setAttribute(Qt::WA_TransparentForMouseEvents); + icon->resize(size, size); + button->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + icon->moveToLeft( + st::settingsSectionIconLeft, + (size.height() - icon->height()) / 2, + size.width()); + }, icon->lifetime()); + + const auto setImage = [=](not_null image) { + state->image = Images::prepare( + image->original(), + size * style::DevicePixelRatio(), + size * style::DevicePixelRatio(), + Images::Option::Smooth | Images::Option::TransparentBackground, + size, + size); + icon->update(); + }; + if (const auto image = state->media->getStickerLarge()) { + setImage(image); + } else { + document->session().downloaderTaskFinished( + ) | rpl::map([=] { + return state->media->getStickerLarge(); + }) | rpl::filter([=](Image *image) { + return (image != nullptr); + }) | rpl::take( + 1 + ) | rpl::start_with_next([=](not_null image) { + setImage(image); + }, button->lifetime()); + } + + icon->paintRequest( + ) | rpl::start_with_next([=] { + Painter p(icon); + const auto width = icon->width(); + if (!state->image.isNull()) { + p.drawImage(0, 0, state->image); + } + }, icon->lifetime()); +} + +} // namespace + +void EditAllowedReactionsBox( + not_null box, + bool isGroup, + const std::vector &list, + const std::vector &selected, + Fn &)> callback) { + box->setTitle(tr::lng_manage_peer_reactions()); + + struct State { + base::flat_map> toggles; + rpl::variable anyToggled; + rpl::event_stream forceToggleAll; + }; + const auto state = box->lifetime().make_state(State{ + .anyToggled = !selected.empty(), + }); + + const auto collect = [=] { + auto result = std::vector(); + result.reserve(state->toggles.size()); + for (const auto &[emoji, button] : state->toggles) { + if (button->toggled()) { + result.push_back(emoji); + } + } + return result; + }; + + const auto container = box->verticalLayout(); + + const auto enabled = Settings::AddButton( + container, + tr::lng_manage_peer_reactions_enable(), + st::manageGroupButton.button); + Ui::CreateChild( + enabled.get(), + st::infoIconReactions, + st::manageGroupButton.iconPosition); + enabled->toggleOn(state->anyToggled.value()); + enabled->toggledChanges( + ) | rpl::filter([=](bool value) { + return (value != state->anyToggled.current()); + }) | rpl::start_to_stream(state->forceToggleAll, enabled->lifetime()); + + Settings::AddSkip(container); + Settings::AddDividerText( + container, + (isGroup + ? tr::lng_manage_peer_reactions_about + : tr::lng_manage_peer_reactions_about_channel)()); + + Settings::AddSkip(container); + Settings::AddSubsectionTitle( + container, + tr::lng_manage_peer_reactions_available()); + + const auto active = [&](const Data::Reaction &entry) { + return ranges::contains(selected, entry.emoji, &Reaction::emoji); + }; + const auto add = [&](const Data::Reaction &entry) { + const auto button = Settings::AddButton( + container, + rpl::single(entry.title), + st::manageGroupButton.button); + AddReactionIcon(button, entry.staticIcon); + state->toggles.emplace(entry.emoji, button); + button->toggleOn(rpl::single( + active(entry) + ) | rpl::then( + state->forceToggleAll.events() + )); + button->toggledChanges( + ) | rpl::start_with_next([=](bool toggled) { + if (toggled) { + state->anyToggled = true; + } else if (collect().empty()) { + state->anyToggled = false; + } + }, button->lifetime()); + }; + for (const auto &entry : list) { + add(entry); + } + + box->addButton(tr::lng_settings_save(), [=] { + const auto ids = collect(); + box->closeBox(); + callback(ids); + }); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); +} + +void SaveAllowedReactions( + not_null peer, + const std::vector &allowed) { + auto ids = allowed | ranges::views::transform([=](QString value) { + return MTP_string(value); + }) | ranges::to; + + peer->session().api().request(MTPmessages_SetChatAvailableReactions( + peer->input, + MTP_vector(ids) + )).done([=](const MTPUpdates &result) { + peer->session().api().applyUpdates(result); + if (const auto chat = peer->asChat()) { + chat->setAllowedReactions(allowed); + } else if (const auto channel = peer->asChannel()) { + channel->setAllowedReactions(allowed); + } else { + Unexpected("Invalid peer type in SaveAllowedReactions."); + } + }).fail([=](const MTP::Error &error) { + if (error.type() == qstr("REACTION_INVALID")) { + peer->updateFullForced(); + peer->owner().reactions().refresh(); + } + }).send(); +} diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.h b/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.h new file mode 100644 index 000000000..917957b7f --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_reactions.h @@ -0,0 +1,27 @@ +/* +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 + +#include "ui/layers/generic_box.h" + +class PeerData; + +namespace Data { +struct Reaction; +} // namespace Data + +void EditAllowedReactionsBox( + not_null box, + bool isGroup, + const std::vector &list, + const std::vector &selected, + Fn &)> callback); + +void SaveAllowedReactions( + not_null peer, + const std::vector &allowed); diff --git a/Telegram/SourceFiles/data/data_changes.h b/Telegram/SourceFiles/data/data_changes.h index 1b8b768f1..9b64579ad 100644 --- a/Telegram/SourceFiles/data/data_changes.h +++ b/Telegram/SourceFiles/data/data_changes.h @@ -86,17 +86,18 @@ struct PeerUpdate { BannedUsers = (1ULL << 25), Rights = (1ULL << 26), PendingRequests = (1ULL << 27), + Reactions = (1ULL << 28), // For channels - ChannelAmIn = (1ULL << 28), - StickersSet = (1ULL << 29), - ChannelLinkedChat = (1ULL << 30), - ChannelLocation = (1ULL << 31), - Slowmode = (1ULL << 32), - GroupCall = (1ULL << 33), + ChannelAmIn = (1ULL << 29), + StickersSet = (1ULL << 30), + ChannelLinkedChat = (1ULL << 31), + ChannelLocation = (1ULL << 32), + Slowmode = (1ULL << 33), + GroupCall = (1ULL << 34), // For iteration - LastUsedBit = (1ULL << 33), + LastUsedBit = (1ULL << 34), }; using Flags = base::flags; friend inline constexpr auto is_flag_type(Flag) { return true; } diff --git a/Telegram/SourceFiles/data/data_channel.cpp b/Telegram/SourceFiles/data/data_channel.cpp index 360755acc..4389c3c30 100644 --- a/Telegram/SourceFiles/data/data_channel.cpp +++ b/Telegram/SourceFiles/data/data_channel.cpp @@ -762,7 +762,10 @@ PeerId ChannelData::groupCallDefaultJoinAs() const { } void ChannelData::setAllowedReactions(std::vector list) { - _allowedReactions = std::move(list); + if (_allowedReactions != list) { + _allowedReactions = std::move(list); + session().changes().peerUpdated(this, UpdateFlag::Reactions); + } } const std::vector &ChannelData::allowedReactions() const { diff --git a/Telegram/SourceFiles/data/data_chat.cpp b/Telegram/SourceFiles/data/data_chat.cpp index 7d5d6bb61..022faaee7 100644 --- a/Telegram/SourceFiles/data/data_chat.cpp +++ b/Telegram/SourceFiles/data/data_chat.cpp @@ -288,7 +288,10 @@ void ChatData::setPendingRequestsCount( } void ChatData::setAllowedReactions(std::vector list) { - _allowedReactions = std::move(list); + if (_allowedReactions != list) { + _allowedReactions = std::move(list); + session().changes().peerUpdated(this, UpdateFlag::Reactions); + } } const std::vector &ChatData::allowedReactions() const { diff --git a/Telegram/SourceFiles/data/data_message_reactions.cpp b/Telegram/SourceFiles/data/data_message_reactions.cpp index 75fc019c1..169a9b915 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.cpp +++ b/Telegram/SourceFiles/data/data_message_reactions.cpp @@ -24,15 +24,19 @@ constexpr auto kRefreshEach = 60 * 60 * crl::time(1000); } // namespace Reactions::Reactions(not_null owner) : _owner(owner) { - request(); + refresh(); base::timer_each( kRefreshEach ) | rpl::start_with_next([=] { - request(); + refresh(); }, _lifetime); } +void Reactions::refresh() { + request(); +} + const std::vector &Reactions::list() const { return _available; } @@ -47,6 +51,10 @@ std::vector Reactions::list(not_null peer) const { } } +rpl::producer<> Reactions::updates() const { + return _updated.events(); +} + std::vector Reactions::Filtered( const std::vector &reactions, const std::vector &emoji) { diff --git a/Telegram/SourceFiles/data/data_message_reactions.h b/Telegram/SourceFiles/data/data_message_reactions.h index af521df1f..2209bb174 100644 --- a/Telegram/SourceFiles/data/data_message_reactions.h +++ b/Telegram/SourceFiles/data/data_message_reactions.h @@ -24,6 +24,8 @@ class Reactions final { public: explicit Reactions(not_null owner); + void refresh(); + [[nodiscard]] const std::vector &list() const; [[nodiscard]] std::vector list(not_null peer) const; diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index f3ebb58df..8fd8a317b 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -362,6 +362,7 @@ infoIconAdministrators: icon {{ "info/edit/group_manage_admins", infoIconFg, poi infoIconBlacklist: icon {{ "info_blacklist", infoIconFg, point(-2px, -2px) }}; infoIconPermissions: icon {{ "info/edit/group_manage_permissions", infoIconFg, point(0px, -2px) }}; infoIconInviteLinks: icon {{ "info/edit/group_manage_links", infoIconFg, point(-2px, 0px) }}; +infoIconReactions: icon {{ "menu/read_reactions", infoIconFg, point(2px, 4px) }}; infoInformationIconPosition: point(25px, 12px); infoNotificationsIconPosition: point(20px, 5px); infoSharedMediaIconPosition: point(20px, 24px); @@ -707,6 +708,11 @@ editPeerInvitesTopSkip: 10px; editPeerInvitesSkip: 10px; editPeerInviteLinkBoxBottomSkip: 15px; +editPeerReactionsButton: SettingsButton(infoProfileButton) { + padding: margins(59px, 13px, 8px, 11px); +} +editPeerReactionsPreview: 28px; + historyTopBarBack: IconButton(infoTopBarBack) { width: 52px; } diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.cpp b/Telegram/SourceFiles/info/profile/info_profile_values.cpp index 248055dac..61e31dd40 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_values.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_values.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "data/data_peer_values.h" #include "data/data_shared_media.h" +#include "data/data_message_reactions.h" #include "data/data_folder.h" #include "data/data_changes.h" #include "data/data_channel.h" @@ -401,6 +402,35 @@ rpl::producer CanAddMemberValue(not_null peer) { return rpl::single(false); } +rpl::producer FullReactionsCountValue( + not_null session) { + const auto reactions = &session->data().reactions(); + return rpl::single( + rpl::empty_value() + ) | rpl::then( + reactions->updates() + ) | rpl::map([=] { + return int(reactions->list().size()); + }) | rpl::distinct_until_changed(); +} + +rpl::producer AllowedReactionsCountValue(not_null peer) { + if (peer->isUser()) { + return FullReactionsCountValue(&peer->session()); + } + return peer->session().changes().peerFlagsValue( + peer, + UpdateFlag::Reactions + ) | rpl::map([=] { + if (const auto chat = peer->asChat()) { + return int(chat->allowedReactions().size()); + } else if (const auto channel = peer->asChannel()) { + return int(channel->allowedReactions().size()); + } + Unexpected("Peer type in AllowedReactionsCountValue."); + }); +} + template rpl::producer BadgeValueFromFlags(Peer peer) { return Data::PeerFlagsValue( diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.h b/Telegram/SourceFiles/info/profile/info_profile_values.h index cd35fae6a..b3313db44 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_values.h +++ b/Telegram/SourceFiles/info/profile/info_profile_values.h @@ -12,6 +12,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL struct ChannelLocation; +namespace Main { +class Session; +} // namespace Main + namespace Ui { class RpWidget; template @@ -63,6 +67,8 @@ rpl::producer SharedMediaCountValue( Storage::SharedMediaType type); rpl::producer CommonGroupsCountValue(not_null user); rpl::producer CanAddMemberValue(not_null peer); +rpl::producer FullReactionsCountValue(not_null peer); +rpl::producer AllowedReactionsCountValue(not_null peer); enum class Badge { None,