From 0f6a5480df88ab13c9fdd9a5f71237be798d88c1 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 19 Feb 2026 21:17:39 +0400 Subject: [PATCH] Implement nice member/admin edit in groups. --- Telegram/Resources/langs/lang.strings | 6 + .../SourceFiles/api/api_chat_participants.cpp | 6 +- .../SourceFiles/api/api_chat_participants.h | 2 +- .../boxes/peers/add_participants_box.cpp | 5 +- .../boxes/peers/edit_participant_box.cpp | 153 +++++++++++++++++- .../boxes/peers/edit_participant_box.h | 4 +- .../boxes/peers/edit_participants_box.cpp | 66 ++++++-- .../boxes/peers/edit_participants_box.h | 1 + .../boxes/peers/edit_tag_control.cpp | 40 +++-- Telegram/SourceFiles/data/data_chat.cpp | 1 + .../admin_log/history_admin_log_inner.cpp | 1 + 11 files changed, 245 insertions(+), 40 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 2f615cd775..b1e49a37ff 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -6016,6 +6016,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_context_edit_my_tag" = "Edit tag"; "lng_context_add_member_tag" = "Add member tag"; "lng_context_edit_member_tag" = "Edit member tag"; +"lng_rights_edit_tag_title" = "Edit tag"; +"lng_rights_tag_about" = "Add short tag next to {name}'s name."; +"lng_rights_tag_about_self" = "Share your role, title, or how you're known in this group. Your tag is visible to all members."; +"lng_rights_promote_member" = "Promote to Admin"; +"lng_rights_remove_member" = "Remove from Group"; +"lng_rights_dismiss_admin" = "Dismiss Admin"; "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}."; diff --git a/Telegram/SourceFiles/api/api_chat_participants.cpp b/Telegram/SourceFiles/api/api_chat_participants.cpp index 4ac53af281..904306929b 100644 --- a/Telegram/SourceFiles/api/api_chat_participants.cpp +++ b/Telegram/SourceFiles/api/api_chat_participants.cpp @@ -670,7 +670,7 @@ void ChatParticipants::Restrict( ChatRestrictionsInfo oldRights, ChatRestrictionsInfo newRights, Fn onDone, - Fn onFail) { + Fn onFail) { channel->session().api().request(MTPchannels_EditBanned( channel->inputChannel(), participant->input(), @@ -681,9 +681,9 @@ void ChatParticipants::Restrict( if (onDone) { onDone(); } - }).fail([=] { + }).fail([=](const MTP::Error &error) { if (onFail) { - onFail(); + onFail(error.type()); } }).send(); } diff --git a/Telegram/SourceFiles/api/api_chat_participants.h b/Telegram/SourceFiles/api/api_chat_participants.h index 4f073eb8e6..4fedab7a75 100644 --- a/Telegram/SourceFiles/api/api_chat_participants.h +++ b/Telegram/SourceFiles/api/api_chat_participants.h @@ -113,7 +113,7 @@ public: ChatRestrictionsInfo oldRights, ChatRestrictionsInfo newRights, Fn onDone, - Fn onFail); + Fn onFail); void add( std::shared_ptr show, not_null peer, diff --git a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp index c0d721f1ee..4b08181573 100644 --- a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp @@ -1595,8 +1595,9 @@ void AddSpecialBoxController::showRestricted( _editParticipantBox->closeBox(); } }); + const auto show = delegate()->peerListUiShow(); box->setSaveCallback( - SaveRestrictedCallback(_peer, user, done, fail)); + SaveRestrictedCallback(show, _peer, user, done, fail)); } _editParticipantBox = showBox(std::move(box)); } @@ -1669,7 +1670,9 @@ void AddSpecialBoxController::kickUser( const auto fail = crl::guard(this, [=] { _editBox = nullptr; }); + const auto show = delegate()->peerListUiShow(); const auto callback = SaveRestrictedCallback( + show, _peer, participant, done, diff --git a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp index 37b749a1aa..33cf8dcd2f 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participant_box.cpp @@ -34,6 +34,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peers/add_bot_to_chat_box.h" #include "boxes/peers/edit_peer_permissions_box.h" #include "boxes/peers/edit_peer_info_box.h" +#include "settings/settings_common.h" #include "data/data_peer_values.h" #include "data/data_channel.h" #include "data/data_chat.h" @@ -44,6 +45,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_layers.h" #include "styles/style_boxes.h" #include "styles/style_info.h" +#include "styles/style_settings.h" namespace { @@ -320,7 +322,8 @@ ChatAdminRightsInfo EditAdminBox::defaultRights() const { | Flag::InviteByLinkOrAdd | Flag::ManageTopics | Flag::PinMessages - | Flag::ManageCall) } + | Flag::ManageCall + | Flag::ManageRanks) } : ChatAdminRightsInfo{ (Flag::ChangeInfo | Flag::PostMessages | Flag::EditMessages @@ -486,7 +489,6 @@ void EditAdminBox::prepare() { Ui::AddSkip(emptyAboutAddAdminsInner->entity()); if (hasRank) { Ui::AddDivider(emptyAboutAddAdminsInner->entity()); - Ui::AddSkip(emptyAboutAddAdminsInner->entity()); } Ui::AddSkip(aboutAddAdminsInner->entity()); Ui::AddDividerText( @@ -525,7 +527,9 @@ void EditAdminBox::prepare() { if (canSave()) { if (hasRank) { - const auto role = LookupBadgeRole(peer(), user()); + const auto role = _oldRights.flags + ? LookupBadgeRole(peer(), user()) + : HistoryView::BadgeRole::Admin; _tagControl = inner->add( object_ptr( inner, @@ -535,6 +539,52 @@ void EditAdminBox::prepare() { role), style::margins()); } + if (_tagControl) { + Ui::AddSkip(inner); + Ui::AddDividerText( + inner, + user()->isSelf() + ? tr::lng_rights_tag_about_self() + : tr::lng_rights_tag_about( + lt_name, + rpl::single(user()->shortName()))); + } + if (_oldRights.flags && !_addingBot) { + const auto isTargetCreator = (LookupBadgeRole(peer(), user()) + == HistoryView::BadgeRole::Creator); + if (!isTargetCreator) { + if (!_tagControl) { + Ui::AddSkip(inner); + inner->add( + object_ptr(inner), + { 0, st::infoProfileSkip, 0, st::infoProfileSkip }); + } + Ui::AddSkip(inner); + const auto dismissButton = Settings::AddButtonWithIcon( + inner, + tr::lng_rights_dismiss_admin(), + st::settingsAttentionButton, + { nullptr }); + dismissButton->setClickedCallback([=] { + if (const auto callback = _saveCallback) { + const auto old = _oldRights; + const auto confirmed = [=](Fn close) { + callback(old, ChatAdminRightsInfo(), QString()); + close(); + }; + uiShow()->showBox( + Ui::MakeConfirmBox({ + .text = tr::lng_profile_sure_remove_admin( + tr::now, + lt_user, + user()->firstName), + .confirmed = confirmed, + .confirmText = tr::lng_box_remove(), + })); + } + }); + } + } _finishSave = [=, value = getChecked] { const auto newFlags = (value() | ChatAdminRight::Other) & ((!channel || channel->amCreator()) @@ -722,6 +772,8 @@ void EditRestrictedBox::prepare() { addControl(std::move(checkboxes), QMargins()); if (canSave() && peer()->canManageRanks()) { + Ui::AddSkip(verticalLayout()); + Ui::AddDivider(verticalLayout()); _tagControl = addControl( object_ptr( this, @@ -730,12 +782,22 @@ void EditRestrictedBox::prepare() { _oldRank, HistoryView::BadgeRole::User), style::margins()); + Ui::AddSkip(verticalLayout()); + Ui::AddDividerText( + verticalLayout(), + user()->isSelf() + ? tr::lng_rights_tag_about_self() + : tr::lng_rights_tag_about( + lt_name, + rpl::single(user()->shortName()))); } _until = prepareRights.until; - addControl( - object_ptr(this, st::defaultVerticalListSkip)); - Ui::AddDivider(verticalLayout()); + if (!_tagControl) { + addControl( + object_ptr(this, st::defaultVerticalListSkip)); + Ui::AddDivider(verticalLayout()); + } addControl( object_ptr( this, @@ -774,6 +836,85 @@ void EditRestrictedBox::prepare() { } if (canSave()) { + Ui::AddSkip(verticalLayout()); + Ui::AddDivider(verticalLayout()); + Ui::AddSkip(verticalLayout()); + + const auto canAddAdmins = (chat && chat->canAddAdmins()) + || (channel && channel->canAddAdmins()); + if (canAddAdmins) { + const auto promoteButton = Settings::AddButtonWithIcon( + verticalLayout(), + tr::lng_rights_promote_member(), + st::settingsButtonNoIcon, + { nullptr }); + promoteButton->setClickedCallback([=] { + const auto rank = _tagControl + ? _tagControl->currentRank() + : _oldRank; + auto adminBox = Box( + peer(), + user(), + ChatAdminRightsInfo(), + rank, + TimeId(0), + nullptr); + const auto adminBoxWeak = QPointer( + adminBox.data()); + const auto show = uiShow(); + const auto restrictWeak = QPointer( + this); + const auto closeBoth = [=] { + if (adminBoxWeak) { + adminBoxWeak->closeBox(); + } + if (restrictWeak) { + restrictWeak->closeBox(); + } + }; + const auto done = [=]( + ChatAdminRightsInfo, + const QString &) { + closeBoth(); + }; + const auto fail = closeBoth; + adminBox->setSaveCallback( + SaveAdminCallback( + show, + peer(), + user(), + done, + fail)); + show->showBox(std::move(adminBox)); + }); + } + + const auto removeButton = Settings::AddButtonWithIcon( + verticalLayout(), + tr::lng_rights_remove_member(), + st::settingsAttentionButton, + { nullptr }); + removeButton->setClickedCallback([=] { + if (const auto callback = _saveCallback) { + const auto old = _oldRights; + const auto confirmed = [=](Fn close) { + callback( + old, + ChannelData::KickedRestrictedRights(user())); + close(); + }; + uiShow()->show( + Ui::MakeConfirmBox({ + .text = tr::lng_profile_sure_kick( + tr::now, + lt_user, + user()->firstName), + .confirmed = confirmed, + .confirmText = tr::lng_box_remove(), + })); + } + }); + const auto save = [=, value = getRestrictions] { if (!_saveCallback) { return; diff --git a/Telegram/SourceFiles/boxes/peers/edit_participant_box.h b/Telegram/SourceFiles/boxes/peers/edit_participant_box.h index a791efeb3c..1001f6961f 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participant_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_participant_box.h @@ -97,12 +97,12 @@ private: [[nodiscard]] ChatAdminRightsInfo defaultRights() const; void transferOwnership(); - bool canSave() const { + [[nodiscard]] bool canSave() const { return _saveCallback != nullptr; } void finishAddAdmin(); void refreshButtons(); - bool canTransferOwnership() const; + [[nodiscard]] bool canTransferOwnership() const; not_null*> setupTransferButton( not_null container, bool isGroup); diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp index d09a97af6a..a72a3f4540 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp @@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/chat_style.h" #include "ui/effects/outline_segments.h" #include "ui/layers/generic_box.h" +#include "ui/vertical_list.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/menu/menu_multiline_action.h" #include "ui/widgets/popup_menu.h" @@ -56,6 +57,7 @@ constexpr auto kParticipantsPerPage = 200; constexpr auto kSortByOnlineDelay = crl::time(1000); void RemoveAdmin( + std::shared_ptr show, not_null channel, not_null user, ChatAdminRightsInfo oldRights, @@ -73,7 +75,10 @@ void RemoveAdmin( if (onDone) { onDone(); } - }).fail([=] { + }).fail([=](const MTP::Error &error) { + if (show) { + show->showToast(error.type()); + } if (onFail) { onFail(); } @@ -140,8 +145,13 @@ void SaveChatAdmin( onFail, false); }, onFail); - } else if (onFail) { - onFail(); + } else { + if (show) { + show->showToast(error.type()); + } + if (onFail) { + onFail(); + } } }).send(); } @@ -175,6 +185,7 @@ void SaveChannelAdmin( } void SaveChatParticipantKick( + std::shared_ptr show, not_null chat, not_null user, Fn onDone, @@ -188,7 +199,10 @@ void SaveChatParticipantKick( if (onDone) { onDone(); } - }).fail([=] { + }).fail([=](const MTP::Error &error) { + if (show) { + show->showToast(error.type()); + } if (onFail) { onFail(); } @@ -297,6 +311,7 @@ Fn SaveRestrictedCallback( + std::shared_ptr show, not_null peer, not_null participant, Fn onDone, @@ -312,12 +327,20 @@ FnshowToast(errorType); + } + if (onFail) { + onFail(); + } + }); }; if (const auto chat = peer->asChatNotMigrated()) { if (participant->isUser() && (newRights.flags & ChatRestriction::ViewMessages)) { SaveChatParticipantKick( + show, chat, participant->asUser(), done, @@ -1721,6 +1744,16 @@ void ParticipantsBoxController::rowClicked(not_null row) { && (_peer->isChat() || _peer->isMegagroup()) && user) { showRestricted(user); + } else if (_role == Role::Members + && user + && (_additional.adminRights(user).has_value() + || _additional.isCreator(user)) + && _additional.canAddOrEditAdmin(user)) { + showAdmin(user); + } else if (_role == Role::Members + && user + && _additional.canRestrictParticipant(participant)) { + showRestricted(user); } else { Assert(_navigation != nullptr); if (_role != Role::Profile) { @@ -2008,8 +2041,9 @@ void ParticipantsBoxController::showRestricted(not_null user) { _editParticipantBox->closeBox(); } }); + const auto show = delegate()->peerListUiShow(); box->setSaveCallback( - SaveRestrictedCallback(_peer, user, done, fail)); + SaveRestrictedCallback(show, _peer, user, done, fail)); } _editParticipantBox = showBox(std::move(box)); } @@ -2134,7 +2168,8 @@ void ParticipantsBoxController::removeAdminSure(not_null user) { if (!adminRights) { return; } - RemoveAdmin(channel, user, *adminRights, crl::guard(this, [=] { + const auto show = delegate()->peerListUiShow(); + RemoveAdmin(show, channel, user, *adminRights, crl::guard(this, [=] { editAdminDone( user, ChatAdminRightsInfo(), @@ -2670,14 +2705,7 @@ void EditCustomRankBox( }; const auto state = box->lifetime().make_state(); - const auto hasRank = !currentRank.isEmpty(); - box->setTitle(isSelf - ? (hasRank - ? tr::lng_context_edit_my_tag() - : tr::lng_context_add_my_tag()) - : (hasRank - ? tr::lng_context_edit_member_tag() - : tr::lng_context_add_member_tag())); + box->setTitle(tr::lng_rights_edit_tag_title()); const auto role = LookupBadgeRole(peer, user); const auto control = box->addRow( @@ -2688,6 +2716,14 @@ void EditCustomRankBox( currentRank, role), style::margins()); + Ui::AddSkip(box->verticalLayout()); + Ui::AddDividerText( + box->verticalLayout(), + (isSelf + ? tr::lng_rights_tag_about_self() + : tr::lng_rights_tag_about( + lt_name, + rpl::single(user->shortName())))); const auto field = control->field(); box->setFocusCallback([=] { field->setFocusFast(); }); diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.h b/Telegram/SourceFiles/boxes/peers/edit_participants_box.h index e1599db0ea..7306cd8e79 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.h @@ -44,6 +44,7 @@ Fn SaveRestrictedCallback( + std::shared_ptr show, not_null peer, not_null participant, Fn onDone, diff --git a/Telegram/SourceFiles/boxes/peers/edit_tag_control.cpp b/Telegram/SourceFiles/boxes/peers/edit_tag_control.cpp index 1e3e24f43a..0a9afa0237 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_tag_control.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_tag_control.cpp @@ -203,7 +203,8 @@ private: const std::unique_ptr _style; const std::unique_ptr _delegate; AdminLog::OwnedItem _item; - QPoint _position; + int _topSkip = 0; + int _bottomSkip = 0; Ui::PeerUserpicView _userpic; }; @@ -225,7 +226,8 @@ EditTagControl::PreviewWidget::PreviewWidget( this, _style.get(), [=] { update(); })) -, _position(0, st::msgMargin.bottom()) { +, _topSkip(st::msgMargin.bottom() * 2) +, _bottomSkip(st::msgMargin.bottom() + st::msgMargin.top()) { _style->apply(_theme.get()); _delegate->setTagText(initialText); @@ -242,18 +244,18 @@ EditTagControl::PreviewWidget::PreviewWidget( ) | rpl::filter([=](int w) { return w >= st::msgMinWidth; }) | rpl::on_next([=](int w) { - const auto h = _position.y() + const auto h = _topSkip + _item->resizeGetHeight(w) - + _position.y(); + + _bottomSkip; resize(w, h); }, lifetime()); _history->owner().itemResizeRequest( ) | rpl::on_next([=](not_null item) { if (_item && item == _item->data() && width() >= st::msgMinWidth) { - const auto h = _position.y() + const auto h = _topSkip + _item->resizeGetHeight(width()) - + _position.y(); + + _bottomSkip; resize(width(), h); } }, lifetime()); @@ -282,17 +284,26 @@ void EditTagControl::PreviewWidget::createItem() { _item = std::move(owned); applyBadge(_delegate->tagText()); if (width() >= st::msgMinWidth) { - const auto h = _position.y() + const auto h = _topSkip + _item->resizeGetHeight(width()) - + _position.y(); + + _bottomSkip; resize(width(), h); } } void EditTagControl::PreviewWidget::applyBadge(const QString &text) { - if (_item) { - _item->overrideRightBadge(text, _role); + if (!_item) { + return; } + auto badgeText = text; + if (badgeText.isEmpty()) { + if (_role == HistoryView::BadgeRole::Admin) { + badgeText = tr::lng_admin_badge(tr::now); + } else if (_role == HistoryView::BadgeRole::Creator) { + badgeText = tr::lng_owner_badge(tr::now); + } + } + _item->overrideRightBadge(badgeText, _role); } void EditTagControl::PreviewWidget::setTagText(const QString &text) { @@ -320,11 +331,12 @@ void EditTagControl::PreviewWidget::paintEvent(QPaintEvent *e) { rect(), e->rect(), !window()->isActiveWindow()); - p.translate(_position); + p.translate(0, _topSkip); _item->draw(p, context); if (_item->displayFromPhoto()) { auto userpicBottom = height() + - _bottomSkip - _item->marginBottom() - _item->marginTop(); const auto userpicTop = userpicBottom - st::msgPhotoSize; @@ -354,7 +366,11 @@ EditTagControl::EditTagControl( , _field(Ui::CreateChild( this, st::customBadgeField, - tr::lng_rights_edit_admin_rank_name(), + (role == HistoryView::BadgeRole::Admin + ? tr::lng_admin_badge() + : role == HistoryView::BadgeRole::Creator + ? tr::lng_owner_badge() + : tr::lng_rights_edit_admin_rank_name()), TextUtilities::RemoveEmoji(currentRank))) { _field->setMaxLength(kRankLimit); _field->setInstantReplaces(Ui::InstantReplaces::TextOnly()); diff --git a/Telegram/SourceFiles/data/data_chat.cpp b/Telegram/SourceFiles/data/data_chat.cpp index cd24f785d8..b4a4e50566 100644 --- a/Telegram/SourceFiles/data/data_chat.cpp +++ b/Telegram/SourceFiles/data/data_chat.cpp @@ -60,6 +60,7 @@ ChatAdminRightsInfo ChatData::defaultAdminRights(not_null user) { | Flag::InviteByLinkOrAdd | Flag::PinMessages | Flag::ManageCall + | Flag::ManageRanks | (isCreator ? Flag::AddAdmins : Flag(0))); } diff --git a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp index 471b94da9b..abde46f43d 100644 --- a/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/admin_log/history_admin_log_inner.cpp @@ -1692,6 +1692,7 @@ void InnerWidget::restrictParticipant( restrictParticipantDone(participant, newRights); }; const auto callback = SaveRestrictedCallback( + _controller->uiShow(), _channel, participant, crl::guard(this, done),