Allow choosing allowed reactions in groups / channels.

This commit is contained in:
John Preston 2021-12-13 15:26:19 +04:00
parent bfdbb64295
commit 9c18f7b0e3
13 changed files with 363 additions and 12 deletions

View file

@ -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

View file

@ -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";

View file

@ -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<Ui::RpWidget> createManageGroupButtons();
object_ptr<Ui::RpWidget> createStickersEdit();
bool canEditInformation() const;
[[nodiscard]] bool canEditInformation() const;
[[nodiscard]] bool canEditReactions() const;
void refreshHistoryVisibility();
void showEditPeerTypeBox(
std::optional<rpl::producer<QString>> 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<QString> &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,

View file

@ -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<Ui::RpWidget*> button,
not_null<DocumentData*> document) {
struct State {
std::shared_ptr<Data::DocumentMedia> media;
QImage image;
};
const auto size = st::editPeerReactionsPreview;
const auto state = button->lifetime().make_state<State>(State{
.media = document->createMediaView(),
});
const auto icon = Ui::CreateChild<Ui::RpWidget>(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*> 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*> 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<Ui::GenericBox*> box,
bool isGroup,
const std::vector<Reaction> &list,
const std::vector<Reaction> &selected,
Fn<void(const std::vector<QString> &)> callback) {
box->setTitle(tr::lng_manage_peer_reactions());
struct State {
base::flat_map<QString, not_null<Ui::SettingsButton*>> toggles;
rpl::variable<bool> anyToggled;
rpl::event_stream<bool> forceToggleAll;
};
const auto state = box->lifetime().make_state<State>(State{
.anyToggled = !selected.empty(),
});
const auto collect = [=] {
auto result = std::vector<QString>();
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<Info::Profile::FloatingIcon>(
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<PeerData*> peer,
const std::vector<QString> &allowed) {
auto ids = allowed | ranges::views::transform([=](QString value) {
return MTP_string(value);
}) | ranges::to<QVector>;
peer->session().api().request(MTPmessages_SetChatAvailableReactions(
peer->input,
MTP_vector<MTPstring>(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();
}

View file

@ -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<Ui::GenericBox*> box,
bool isGroup,
const std::vector<Data::Reaction> &list,
const std::vector<Data::Reaction> &selected,
Fn<void(const std::vector<QString> &)> callback);
void SaveAllowedReactions(
not_null<PeerData*> peer,
const std::vector<QString> &allowed);

View file

@ -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<Flag>;
friend inline constexpr auto is_flag_type(Flag) { return true; }

View file

@ -762,7 +762,10 @@ PeerId ChannelData::groupCallDefaultJoinAs() const {
}
void ChannelData::setAllowedReactions(std::vector<QString> list) {
_allowedReactions = std::move(list);
if (_allowedReactions != list) {
_allowedReactions = std::move(list);
session().changes().peerUpdated(this, UpdateFlag::Reactions);
}
}
const std::vector<QString> &ChannelData::allowedReactions() const {

View file

@ -288,7 +288,10 @@ void ChatData::setPendingRequestsCount(
}
void ChatData::setAllowedReactions(std::vector<QString> list) {
_allowedReactions = std::move(list);
if (_allowedReactions != list) {
_allowedReactions = std::move(list);
session().changes().peerUpdated(this, UpdateFlag::Reactions);
}
}
const std::vector<QString> &ChatData::allowedReactions() const {

View file

@ -24,15 +24,19 @@ constexpr auto kRefreshEach = 60 * 60 * crl::time(1000);
} // namespace
Reactions::Reactions(not_null<Session*> owner) : _owner(owner) {
request();
refresh();
base::timer_each(
kRefreshEach
) | rpl::start_with_next([=] {
request();
refresh();
}, _lifetime);
}
void Reactions::refresh() {
request();
}
const std::vector<Reaction> &Reactions::list() const {
return _available;
}
@ -47,6 +51,10 @@ std::vector<Reaction> Reactions::list(not_null<PeerData*> peer) const {
}
}
rpl::producer<> Reactions::updates() const {
return _updated.events();
}
std::vector<Reaction> Reactions::Filtered(
const std::vector<Reaction> &reactions,
const std::vector<QString> &emoji) {

View file

@ -24,6 +24,8 @@ class Reactions final {
public:
explicit Reactions(not_null<Session*> owner);
void refresh();
[[nodiscard]] const std::vector<Reaction> &list() const;
[[nodiscard]] std::vector<Reaction> list(not_null<PeerData*> peer) const;

View file

@ -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;
}

View file

@ -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<bool> CanAddMemberValue(not_null<PeerData*> peer) {
return rpl::single(false);
}
rpl::producer<int> FullReactionsCountValue(
not_null<Main::Session*> 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<int> AllowedReactionsCountValue(not_null<PeerData*> 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 <typename Flag, typename Peer>
rpl::producer<Badge> BadgeValueFromFlags(Peer peer) {
return Data::PeerFlagsValue(

View file

@ -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 <typename Widget>
@ -63,6 +67,8 @@ rpl::producer<int> SharedMediaCountValue(
Storage::SharedMediaType type);
rpl::producer<int> CommonGroupsCountValue(not_null<UserData*> user);
rpl::producer<bool> CanAddMemberValue(not_null<PeerData*> peer);
rpl::producer<int> FullReactionsCountValue(not_null<Main::Session*> peer);
rpl::producer<int> AllowedReactionsCountValue(not_null<PeerData*> peer);
enum class Badge {
None,