Show custom ranks, allow toggling on.

This commit is contained in:
John Preston
2026-02-10 16:39:56 +04:00
parent 62d32a1bdf
commit 7969795a01
18 changed files with 270 additions and 50 deletions
+3
View File
@@ -5981,6 +5981,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_rights_edit_admin_header" = "What can this admin do?";
"lng_rights_edit_admin_rank_name" = "Custom title";
"lng_rights_edit_admin_rank_about" = "A title that members will see instead of '{title}'.";
"lng_manage_peer_custom_ranks_title" = "Custom Member Titles";
"lng_manage_peer_custom_ranks" = "Allow Custom Titles";
"lng_manage_peer_custom_ranks_about" = "Allow members to set custom titles that will be shown next to their names in messages.";
"lng_rights_about_add_admins_yes" = "This admin will be able to add new admins with equal or fewer rights.";
"lng_rights_about_add_admins_no" = "This admin will not be able to add new admins.";
"lng_rights_about_by" = "This admin promoted by {user} on {date}.";
@@ -112,6 +112,7 @@ void ApplyLastList(
channel->mgInfo->lastAdmins.clear();
channel->mgInfo->lastRestricted.clear();
channel->mgInfo->lastParticipants.clear();
channel->mgInfo->memberRanks.clear();
channel->mgInfo->lastParticipantsStatus
= MegagroupInfo::LastParticipantsUpToDate
| MegagroupInfo::LastParticipantsOnceReceived;
@@ -148,6 +149,9 @@ void ApplyLastList(
channel->mgInfo->botStatus = 2;
}
}
if (!p.rank().isEmpty() && !p.isCreatorOrAdmin()) {
channel->mgInfo->memberRanks[p.userId()] = p.rank();
}
}
}
//
@@ -287,12 +291,14 @@ ChatParticipant::ChatParticipant(
_type = Type::Member;
_date = data.vdate().v;
_by = peerToUser(peerFromUser(data.vinviter_id()));
_rank = qs(data.vrank().value_or_empty());
if (data.vsubscription_until_date()) {
_subscriptionDate = data.vsubscription_until_date()->v;
}
}, [&](const MTPDchannelParticipant &data) {
_type = Type::Member;
_date = data.vdate().v;
_rank = qs(data.vrank().value_or_empty());
if (data.vsubscription_until_date()) {
_subscriptionDate = data.vsubscription_until_date()->v;
}
+4
View File
@@ -1894,6 +1894,10 @@ void Updates::feedUpdate(const MTPUpdate &update) {
session().data().applyUpdate(update.c_updateChatParticipantAdmin());
} break;
case mtpc_updateChatParticipantRank: {
session().data().applyUpdate(update.c_updateChatParticipantRank());
} break;
case mtpc_updateChatDefaultBannedRights: {
session().data().applyUpdate(update.c_updateChatDefaultBannedRights());
} break;
@@ -389,6 +389,12 @@ QString ParticipantsAdditionalData::adminRank(
return (i != end(_adminRanks)) ? i->second : QString();
}
QString ParticipantsAdditionalData::memberRank(
not_null<UserData*> user) const {
const auto i = _memberRanks.find(user);
return (i != end(_memberRanks)) ? i->second : QString();
}
TimeId ParticipantsAdditionalData::adminPromotedSince(
not_null<UserData*> user) const {
const auto i = _adminPromotedSince.find(user);
@@ -626,6 +632,7 @@ PeerData *ParticipantsAdditionalData::applyParticipant(
return nullptr;
};
const auto result = [&]() -> PeerData* {
switch (data.type()) {
case Api::ChatParticipant::Type::Creator: {
if (overrideRole != Role::Profile
@@ -663,6 +670,15 @@ PeerData *ParticipantsAdditionalData::applyParticipant(
return logBad();
};
Unexpected("Api::ChatParticipant::type in applyParticipant.");
}();
if (const auto user = result ? result->asUser() : nullptr) {
if (!data.rank().isEmpty() && !data.isCreatorOrAdmin()) {
_memberRanks[user] = data.rank();
} else {
_memberRanks.remove(user);
}
}
return result;
}
UserData *ParticipantsAdditionalData::applyCreator(
@@ -2135,7 +2151,10 @@ auto ParticipantsBoxController::computeType(
&& user
&& user->isInaccessible();
if (!result.canRemove) {
result.adminRank = user ? _additional.adminRank(user) : QString();
const auto aRank = user ? _additional.adminRank(user) : QString();
result.adminRank = !aRank.isEmpty()
? aRank
: (user ? _additional.memberRank(user) : QString());
}
return result;
}
@@ -107,6 +107,7 @@ public:
[[nodiscard]] std::optional<ChatAdminRightsInfo> adminRights(
not_null<UserData*> user) const;
[[nodiscard]] QString adminRank(not_null<UserData*> user) const;
[[nodiscard]] QString memberRank(not_null<UserData*> user) const;
[[nodiscard]] std::optional<ChatRestrictionsInfo> restrictedRights(
not_null<PeerData*> participant) const;
[[nodiscard]] bool isCreator(not_null<UserData*> user) const;
@@ -149,6 +150,7 @@ private:
// Data for channels.
base::flat_map<not_null<UserData*>, ChatAdminRightsInfo> _adminRights;
base::flat_map<not_null<UserData*>, QString> _adminRanks;
base::flat_map<not_null<UserData*>, QString> _memberRanks;
base::flat_map<not_null<UserData*>, TimeId> _adminPromotedSince;
base::flat_map<not_null<PeerData*>, TimeId> _restrictedSince;
base::flat_map<not_null<UserData*>, TimeId> _memberSince;
@@ -405,6 +405,7 @@ private:
std::optional<bool> signatures;
std::optional<bool> signatureProfiles;
std::optional<bool> noForwards;
std::optional<bool> customRanks;
std::optional<bool> joinToWrite;
std::optional<bool> requestToJoin;
std::optional<ChannelData*> discussionLink;
@@ -465,6 +466,7 @@ private:
[[nodiscard]] bool validateAutotranslate(Saving &to) const;
[[nodiscard]] bool validateSignatures(Saving &to) const;
[[nodiscard]] bool validateForwards(Saving &to) const;
[[nodiscard]] bool validateCustomRanks(Saving &to) const;
[[nodiscard]] bool validateJoinToWrite(Saving &to) const;
[[nodiscard]] bool validateRequestToJoin(Saving &to) const;
@@ -480,6 +482,7 @@ private:
void saveAutotranslate();
void saveSignatures();
void saveForwards();
void saveCustomRanks();
void saveJoinToWrite();
void saveRequestToJoin();
void savePhoto();
@@ -999,6 +1002,11 @@ void Controller::fillPrivacyTypeButton() {
? _peer->asChannel()->usernames()
: std::vector<QString>()),
.noForwards = !_peer->allowsForwarding(),
.customRanks = (_peer->isChat()
? _peer->asChat()->customRanksEnabled()
: _peer->isChannel()
? _peer->asChannel()->customRanksEnabled()
: false),
.joinToWrite = (_peer->isMegagroup()
&& _peer->asChannel()->joinToWrite()),
.requestToJoin = (_peer->isMegagroup()
@@ -2096,6 +2104,7 @@ std::optional<Controller::Saving> Controller::validate() const {
&& validateAutotranslate(result)
&& validateSignatures(result)
&& validateForwards(result)
&& validateCustomRanks(result)
&& validateJoinToWrite(result)
&& validateRequestToJoin(result)) {
return result;
@@ -2218,6 +2227,14 @@ bool Controller::validateForwards(Saving &to) const {
return true;
}
bool Controller::validateCustomRanks(Saving &to) const {
if (!_typeDataSavedValue) {
return true;
}
to.customRanks = _typeDataSavedValue->customRanks;
return true;
}
bool Controller::validateJoinToWrite(Saving &to) const {
if (!_typeDataSavedValue) {
return true;
@@ -2253,6 +2270,7 @@ void Controller::save() {
pushSaveStage([=] { saveAutotranslate(); });
pushSaveStage([=] { saveSignatures(); });
pushSaveStage([=] { saveForwards(); });
pushSaveStage([=] { saveCustomRanks(); });
pushSaveStage([=] { saveJoinToWrite(); });
pushSaveStage([=] { saveRequestToJoin(); });
pushSaveStage([=] { savePhoto(); });
@@ -2761,6 +2779,32 @@ void Controller::saveForwards() {
}).send();
}
void Controller::saveCustomRanks() {
const auto isEnabled = _peer->isChat()
? _peer->asChat()->customRanksEnabled()
: _peer->isChannel()
? _peer->asChannel()->customRanksEnabled()
: false;
if (!_savingData.customRanks
|| *_savingData.customRanks == isEnabled) {
return continueSave();
}
_api.request(MTPmessages_ToggleChatCustomRanks(
_peer->input(),
MTP_bool(*_savingData.customRanks)
)).done([=](const MTPUpdates &result) {
_peer->session().api().applyUpdates(result);
continueSave();
}).fail([=](const MTP::Error &error) {
if (error.type() == u"CHAT_NOT_MODIFIED"_q) {
continueSave();
} else {
_navigation->showToast(error.type());
cancelSave();
}
}).send();
}
void Controller::saveJoinToWrite() {
const auto joinToWrite = _peer->isMegagroup()
&& _peer->asChannel()->joinToWrite();
@@ -83,6 +83,10 @@ public:
[[nodiscard]] bool noForwards() const {
return _controls.noForwards->toggled();
}
[[nodiscard]] bool customRanks() const {
return _controls.customRanks
&& _controls.customRanks->toggled();
}
[[nodiscard]] bool joinToWrite() const {
return _controls.joinToWrite && _controls.joinToWrite->toggled();
}
@@ -112,6 +116,7 @@ private:
Ui::SlideWrap<Ui::VerticalLayout> *whoSendWrap = nullptr;
Ui::SettingsButton *noForwards = nullptr;
Ui::SettingsButton *customRanks = nullptr;
Ui::SettingsButton *joinToWrite = nullptr;
Ui::SettingsButton *requestToJoin = nullptr;
};
@@ -296,6 +301,35 @@ void Controller::createContent() {
(_isGroup
? tr::lng_manage_peer_no_forwards_about
: tr::lng_manage_peer_no_forwards_about_channel)());
const auto amCreator = (_peer->isChat()
&& _peer->asChat()->amCreator())
|| (_peer->isChannel()
&& _peer->asChannel()->amCreator());
if (amCreator) {
Ui::AddSkip(_wrap.get());
Ui::AddSubsectionTitle(
_wrap.get(),
tr::lng_manage_peer_custom_ranks_title());
_controls.customRanks = _wrap->add(
EditPeerInfoBox::CreateButton(
_wrap.get(),
tr::lng_manage_peer_custom_ranks(),
rpl::single(QString()),
[] {},
st::peerPermissionsButton,
{}));
_controls.customRanks->toggleOn(
rpl::single(_dataSavedValue->customRanks)
)->toggledValue(
) | rpl::on_next([=](bool toggled) {
_dataSavedValue->customRanks = toggled;
}, _wrap->lifetime());
Ui::AddSkip(_wrap.get());
Ui::AddDividerText(
_wrap.get(),
tr::lng_manage_peer_custom_ranks_about());
}
}
if (_linkOnly) {
_controls.inviteLinkWrap->show(anim::type::instant);
@@ -777,6 +811,7 @@ void EditPeerTypeBox::prepare() {
? controller->usernamesOrder()
: std::vector<QString>()),
.noForwards = controller->noForwards(),
.customRanks = controller->customRanks(),
.joinToWrite = controller->joinToWrite(),
.requestToJoin = controller->requestToJoin(),
}); // We don't need username with private type.
@@ -39,6 +39,7 @@ struct EditPeerTypeData {
std::vector<QString> usernamesOrder;
bool hasDiscussionLink = false;
bool noForwards = false;
bool customRanks = false;
bool joinToWrite = false;
bool requestToJoin = false;
};
@@ -701,6 +701,10 @@ bool ChannelData::allowsForwarding() const {
return !(flags() & Flag::NoForwards);
}
bool ChannelData::customRanksEnabled() const {
return flags() & Flag::CustomRanksEnabled;
}
bool ChannelData::canViewMembers() const {
return (flags() & Flag::CanViewParticipants)
&& (!(flags() & Flag::ParticipantsHidden)
+3
View File
@@ -86,6 +86,7 @@ enum class ChannelDataFlag : uint64 {
HasStarsPerMessage = (1ULL << 43),
StarsPerMessageKnown = (1ULL << 44),
HasActiveVideoStream = (1ULL << 45),
CustomRanksEnabled = (1ULL << 46),
};
inline constexpr bool is_flag_type(ChannelDataFlag) { return true; };
using ChannelDataFlags = base::flags<ChannelDataFlag>;
@@ -143,6 +144,7 @@ public:
// For admin badges, full admins list with ranks.
base::flat_map<UserId, QString> admins;
base::flat_map<UserId, QString> memberRanks;
UserData *creator = nullptr; // nullptr means unknown
QString creatorRank;
@@ -355,6 +357,7 @@ public:
[[nodiscard]] bool autoTranslation() const {
return flags() & Flag::AutoTranslation;
}
[[nodiscard]] bool customRanksEnabled() const;
[[nodiscard]] auto adminRights() const {
return _adminRights.current();
+37 -1
View File
@@ -67,6 +67,10 @@ bool ChatData::allowsForwarding() const {
return !(flags() & Flag::NoForwards);
}
bool ChatData::customRanksEnabled() const {
return flags() & Flag::CustomRanksEnabled;
}
bool ChatData::canEditInformation() const {
return amIn() && !amRestricted(ChatRestriction::ChangeInfo);
}
@@ -437,6 +441,25 @@ void ApplyChatUpdate(
session->changes().peerUpdated(chat, UpdateFlag::Admins);
}
void ApplyChatUpdate(
not_null<ChatData*> chat,
const MTPDupdateChatParticipantRank &update) {
if (chat->applyUpdateVersion(update.vversion().v)
!= ChatData::UpdateStatus::Good) {
return;
}
const auto rank = qs(update.vrank().v);
const auto userId = UserId(update.vuser_id().v);
if (rank.isEmpty()) {
chat->memberRanks.remove(userId);
} else {
chat->memberRanks[userId] = rank;
}
chat->session().changes().peerUpdated(
chat,
Data::PeerUpdate::Flag::Members);
}
void ApplyChatUpdate(
not_null<ChatData*> chat,
const MTPDupdateChatDefaultBannedRights &update) {
@@ -532,6 +555,7 @@ void ApplyChatUpdate(
chat->participants.clear();
chat->invitedByMe.clear();
chat->admins.clear();
chat->memberRanks.clear();
chat->setAdminRights(ChatAdminRights());
const auto selfUserId = session->userId();
for (const auto &participant : list) {
@@ -558,13 +582,25 @@ void ApplyChatUpdate(
participant.match([&](const MTPDchatParticipantCreator &data) {
chat->creator = userId;
const auto rank = qs(data.vrank().value_or_empty());
if (!rank.isEmpty()) {
chat->memberRanks[userId] = rank;
}
}, [&](const MTPDchatParticipantAdmin &data) {
chat->admins.emplace(user);
if (user->isSelf()) {
chat->setAdminRights(
chat->defaultAdminRights(user).flags);
}
}, [](const MTPDchatParticipant &) {
const auto rank = qs(data.vrank().value_or_empty());
if (!rank.isEmpty()) {
chat->memberRanks[userId] = rank;
}
}, [&](const MTPDchatParticipant &data) {
const auto rank = qs(data.vrank().value_or_empty());
if (!rank.isEmpty()) {
chat->memberRanks[userId] = rank;
}
});
}
if (chat->participants.empty()) {
+6
View File
@@ -23,6 +23,7 @@ enum class ChatDataFlag {
CallNotEmpty = (1 << 6),
CanSetUsername = (1 << 7),
NoForwards = (1 << 8),
CustomRanksEnabled = (1 << 9),
};
inline constexpr bool is_flag_type(ChatDataFlag) { return true; };
using ChatDataFlags = base::flags<ChatDataFlag>;
@@ -99,6 +100,7 @@ public:
// Like in ChannelData.
[[nodiscard]] bool allowsForwarding() const;
[[nodiscard]] bool customRanksEnabled() const;
[[nodiscard]] bool canEditInformation() const;
[[nodiscard]] bool canEditPermissions() const;
[[nodiscard]] bool canEditUsername() const;
@@ -178,6 +180,7 @@ public:
base::flat_set<not_null<UserData*>> admins;
std::deque<not_null<UserData*>> lastAuthors;
base::flat_set<not_null<PeerData*>> markupSenders;
base::flat_map<UserId, QString> memberRanks;
int botStatus = 0; // -1 - no bots, 0 - unknown, 1 - one bot, that sees all history, 2 - other
private:
@@ -215,6 +218,9 @@ void ApplyChatUpdate(
void ApplyChatUpdate(
not_null<ChatData*> chat,
const MTPDupdateChatParticipantAdmin &update);
void ApplyChatUpdate(
not_null<ChatData*> chat,
const MTPDupdateChatParticipantRank &update);
void ApplyChatUpdate(
not_null<ChatData*> chat,
const MTPDupdateChatDefaultBannedRights &update);
+14 -4
View File
@@ -866,7 +866,8 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) {
| Flag::Forbidden
| Flag::CallActive
| Flag::CallNotEmpty
| Flag::NoForwards;
| Flag::NoForwards
| Flag::CustomRanksEnabled;
const auto flagsSet = (data.is_left() ? Flag::Left : Flag())
| (data.is_creator() ? Flag::Creator : Flag())
| (data.is_deactivated() ? Flag::Deactivated : Flag())
@@ -876,7 +877,8 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) {
&& chat->groupCall()->fullCount() > 0))
? Flag::CallNotEmpty
: Flag())
| (data.is_noforwards() ? Flag::NoForwards : Flag());
| (data.is_noforwards() ? Flag::NoForwards : Flag())
| (data.is_custom_ranks_enabled() ? Flag::CustomRanksEnabled : Flag());
chat->setFlags((chat->flags() & ~flagsMask) | flagsSet);
chat->count = data.vparticipants_count().v;
@@ -997,7 +999,8 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) {
| Flag::AutoTranslation
| Flag::Monoforum
| Flag::HasStarsPerMessage
| Flag::StarsPerMessageKnown;
| Flag::StarsPerMessageKnown
| Flag::CustomRanksEnabled;
const auto hasStarsPerMessage
= data.vsend_paid_messages_stars().has_value();
if (!hasStarsPerMessage) {
@@ -1055,7 +1058,8 @@ not_null<PeerData*> Session::processChat(const MTPChat &data) {
| (channel->starsPerMessageKnown()
? Flag::StarsPerMessageKnown
: Flag()))
: Flag::StarsPerMessageKnown);
: Flag::StarsPerMessageKnown)
| (data.is_custom_ranks_enabled() ? Flag::CustomRanksEnabled : Flag());
channel->setFlags((channel->flags() & ~flagsMask) | flagsSet);
channel->setBotVerifyDetailsIcon(
data.vbot_verification_icon().value_or_empty());
@@ -4407,6 +4411,12 @@ void Session::applyUpdate(const MTPDupdateChatParticipantAdmin &update) {
}
}
void Session::applyUpdate(const MTPDupdateChatParticipantRank &update) {
if (const auto chat = chatLoaded(update.vchat_id().v)) {
ApplyChatUpdate(chat, update);
}
}
void Session::applyUpdate(const MTPDupdateChatDefaultBannedRights &update) {
if (const auto peer = peerLoaded(peerFromMTP(update.vpeer()))) {
if (const auto chat = peer->asChat()) {
+1
View File
@@ -469,6 +469,7 @@ public:
void applyUpdate(const MTPDupdateChatParticipantAdd &update);
void applyUpdate(const MTPDupdateChatParticipantDelete &update);
void applyUpdate(const MTPDupdateChatParticipantAdmin &update);
void applyUpdate(const MTPDupdateChatParticipantRank &update);
void applyUpdate(const MTPDupdateChatDefaultBannedRights &update);
void applyDialogs(
@@ -478,6 +478,13 @@ HistoryItem::HistoryItem(
}
}
if (const auto rank = data.vfrom_rank()) {
if (!rank->v.isEmpty()) {
AddComponents(HistoryMessageFromRank::Bit());
Get<HistoryMessageFromRank>()->rank = qs(*rank);
}
}
if (const auto until = data.vreport_delivery_until_date()) {
if (base::unixtime::now() < TimeId(until->v)) {
history->owner().histories().reportDelivery(this);
@@ -3276,6 +3283,13 @@ QString HistoryItem::originalPostAuthor() const {
return QString();
}
QString HistoryItem::fromRank() const {
if (const auto component = Get<HistoryMessageFromRank>()) {
return component->rank;
}
return QString();
}
MsgId HistoryItem::originalId() const {
if (const auto forwarded = Get<HistoryMessageForwarded>()) {
return forwarded->originalId;
@@ -604,6 +604,8 @@ public:
return _boostsApplied;
}
[[nodiscard]] QString fromRank() const;
MsgId id;
private:
@@ -103,6 +103,11 @@ struct HistoryMessageSigned
bool isAnonymousRank = false;
};
struct HistoryMessageFromRank
: RuntimeComponent<HistoryMessageFromRank, HistoryItem> {
QString rank;
};
struct HistoryMessageEdited
: RuntimeComponent<HistoryMessageEdited, HistoryItem> {
TimeId date = 0;
@@ -39,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/components/sponsored_messages.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "data/data_chat.h"
#include "data/data_channel.h"
#include "data/data_forum_topic.h"
#include "data/data_message_reactions.h"
@@ -272,23 +273,47 @@ void Message::refreshRightBadge() {
}
const auto channel = item->history()->peer->asMegagroup();
const auto user = item->author()->asUser();
if (!channel || !user) {
if (!channel) {
if (const auto chat = item->history()->peer->asChat()) {
if (user) {
const auto j = chat->memberRanks.find(
peerToUser(user->id));
if (j != chat->memberRanks.end()) {
return j->second;
}
}
}
return QString();
}
if (!user) {
return QString();
}
const auto info = channel->mgInfo.get();
const auto i = info->admins.find(peerToUser(user->id));
const auto userId = peerToUser(user->id);
const auto i = info->admins.find(userId);
const auto custom = (i != info->admins.end())
? i->second
: (info->creator == user)
? info->creatorRank
: QString();
return !custom.isEmpty()
? custom
: (info->creator == user)
? tr::lng_owner_badge(tr::now)
: (i != info->admins.end())
? tr::lng_admin_badge(tr::now)
: QString();
if (!custom.isEmpty()) {
return custom;
}
if (info->creator == user) {
return tr::lng_owner_badge(tr::now);
}
if (i != info->admins.end()) {
return tr::lng_admin_badge(tr::now);
}
const auto fromRank = item->fromRank();
if (!fromRank.isEmpty()) {
return fromRank;
}
const auto j = info->memberRanks.find(userId);
if (j != info->memberRanks.end()) {
return j->second;
}
return QString();
}();
auto badge = TextWithEntities{
(text.isEmpty()