From edc3006fbea91283ec0c3d8998f7be9117d20114 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 18 Feb 2026 21:52:00 +0400 Subject: [PATCH] Improve Remove/Delete from group participants. --- .../boxes/peers/edit_participants_box.cpp | 222 +++++---- .../boxes/peers/edit_participants_box.h | 18 +- .../group/calls_group_invite_controller.cpp | 4 +- .../giveaway/giveaway_list_controllers.cpp | 7 +- Telegram/SourceFiles/info/info.style | 2 +- .../info_profile_members_controllers.cpp | 432 +++++++++++++----- .../info_profile_members_controllers.h | 85 +++- Telegram/SourceFiles/ui/chat/chat.style | 2 +- 8 files changed, 542 insertions(+), 230 deletions(-) diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp index 6a0b367810..4400a8474c 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp @@ -945,13 +945,13 @@ ParticipantsBoxController::ParticipantsBoxController( not_null peer, Role role) : PeerListController(CreateSearchController(peer, role, &_additional)) +, _chatStyle( + std::make_unique(peer->session().colorIndicesValue())) , _navigation(navigation) , _peer(peer) , _api(&_peer->session().mtp()) , _role(role) -, _additional(peer, _role) -, _chatStyle( - std::make_unique(peer->session().colorIndicesValue())) { +, _additional(peer, _role) { subscribeToMigration(); if (_role == Role::Profile) { setupListChangeViewers(); @@ -1157,18 +1157,20 @@ void ParticipantsBoxController::peerListSearchAddRow( std::unique_ptr ParticipantsBoxController::createSearchRow( not_null peer) { - if (const auto user = peer->asUser()) { - return createRow(user); + if (_role == Role::Profile + || _role == Role::Members + || _role == Role::Admins) { + if (const auto user = peer->asUser()) { + return createRow(user); + } + return nullptr; } - return nullptr; + return createRow(peer); } std::unique_ptr ParticipantsBoxController::createRestoredRow( not_null peer) { - if (const auto user = peer->asUser()) { - return createRow(user); - } - return nullptr; + return createSearchRow(peer); } auto ParticipantsBoxController::saveState() const @@ -1550,14 +1552,11 @@ void ParticipantsBoxController::chatListReady() { } void ParticipantsBoxController::rebuildRowTypes() { - if (_role != Role::Profile) { - return; - } const auto count = delegate()->peerListFullRowsCount(); for (auto i = 0; i != count; ++i) { const auto row = static_cast( delegate()->peerListRowAt(i).get()); - row->setType(computeType(row->user())); + row->setType(computeType(row->peer())); } refreshRows(); } @@ -1734,35 +1733,46 @@ void ParticipantsBoxController::rowClicked(not_null row) { void ParticipantsBoxController::rowRightActionClicked( not_null row) { + rowElementClicked(row, Row::kRemoveElement); +} + +void ParticipantsBoxController::rowElementClicked( + not_null row, + int element) { const auto participant = row->peer(); const auto user = participant->asUser(); - if (_role == Role::Members || _role == Role::Profile) { - if (_role == Role::Profile && user) { - const auto memberRow = static_cast(row.get()); - if (memberRow->type().canAddTag) { - const auto show = delegate()->peerListUiShow(); - const auto peer = _peer; - _editBox = show->show(Box( - EditCustomRankBox, - show, - peer, - user, - QString(), - true, - crl::guard(this, [=](const QString &rank) { - _additional.applyMemberRankLocally(user, rank); - recomputeTypeFor(user); - refreshRows(); - }))); - return; - } + const auto memberRow = static_cast(row.get()); + if (element == Row::kTagElement) { + if (!user) { + return; + } + if (memberRow->type().canAddTag || memberRow->type().canEditTag) { + const auto show = delegate()->peerListUiShow(); + const auto peer = _peer; + const auto currentRank = _additional.memberRank(user); + const auto isSelf = user->isSelf(); + _editBox = show->show(Box( + EditCustomRankBox, + show, + peer, + user, + currentRank, + isSelf, + crl::guard(this, [=](const QString &rank) { + _additional.applyMemberRankLocally(user, rank); + recomputeTypeFor(user); + refreshRows(); + }))); + } + } else if (element == Row::kRemoveElement) { + if (_role == Role::Members || _role == Role::Profile) { + kickParticipant(participant); + } else if (_role == Role::Admins) { + Assert(user != nullptr); + removeAdmin(user); + } else { + removeKicked(row, participant); } - kickParticipant(participant); - } else if (_role == Role::Admins) { - Assert(user != nullptr); - removeAdmin(user); - } else { - removeKicked(row, participant); } } @@ -2223,40 +2233,26 @@ bool ParticipantsBoxController::removeRow(not_null participant) { std::unique_ptr ParticipantsBoxController::createRow( not_null participant) const { - const auto user = participant->asUser(); if (_role == Role::Profile) { - Assert(user != nullptr); - return std::make_unique(user, computeType(user)); + Assert(participant->asUser() != nullptr); } - const auto chat = _peer->asChat(); - const auto channel = _peer->asChannel(); - auto row = std::make_unique(participant); + auto row = std::make_unique(participant, computeType(participant)); refreshCustomStatus(row.get()); - if (_role == Role::Admins - && user - && !_additional.isCreator(user) - && _additional.adminRights(user).has_value() - && _additional.canEditAdmin(user)) { - row->setActionLink(tr::lng_profile_kick(tr::now)); - } else if (_role == Role::Kicked || _role == Role::Restricted) { - if (_additional.canRestrictParticipant(participant)) { - row->setActionLink(tr::lng_profile_delete_removed(tr::now)); - } - } else if (_role == Role::Members) { - Assert(user != nullptr); - if ((chat ? chat->canBanMembers() : channel->canBanMembers()) - && !_additional.isCreator(user) - && (!_additional.adminRights(user) - || _additional.canEditAdmin(user))) { - row->setActionLink(tr::lng_profile_kick(tr::now)); - } - if (_role == Role::Members && user->isBot()) { - auto seesAllMessages = (user->botInfo->readsAllHistory || _additional.adminRights(user).has_value()); + if (const auto user = participant->asUser()) { + if ((_role == Role::Members || _role == Role::Profile) + && user->isBot()) { + const auto seesAllMessages = + (user->botInfo->readsAllHistory + || _additional.adminRights(user).has_value()); row->setCustomStatus(seesAllMessages ? tr::lng_status_bot_reads_all(tr::now) : tr::lng_status_bot_not_reads_all(tr::now)); } } + const auto raw = row.get(); + row->setRefreshCallback(crl::guard(this, [=] { + delegate()->peerListUpdateRow(raw); + })); return row; } @@ -2272,28 +2268,92 @@ auto ParticipantsBoxController::computeType( : (user && _additional.adminRights(user).has_value()) ? Rights::Admin : Rights::Normal; - result.canRemove = _additional.canRemoveParticipant(participant) - && user - && user->isInaccessible(); - if (!result.canRemove) { - result.rank = user ? _additional.memberRank(user) : QString(); + + if (user) { + result.rank = _additional.memberRank(user); } - if (user - && user->isSelf() - && result.rank.isEmpty() - && !result.canRemove - && result.rights == Rights::Normal - && !_peer->amRestricted(ChatRestriction::EditRank)) { - result.canAddTag = true; + + const auto chat = _peer->asChat(); + const auto channel = _peer->asChannel(); + + switch (_role) { + case Role::Profile: { + if (user + && (chat + ? chat->canBanMembers() + : (channel && channel->canBanMembers())) + && !_additional.isCreator(user) + && (!_additional.adminRights(user) + || _additional.canEditAdmin(user))) { + result.canRemove = true; + result.removeText = tr::lng_profile_kick(tr::now); + } else if (_additional.canRemoveParticipant(participant) + && user + && user->isInaccessible()) { + result.canRemove = true; + result.rank = QString(); + result.removeText = tr::lng_profile_delete_removed(tr::now); + } + } break; + case Role::Members: { + if (user + && (chat + ? chat->canBanMembers() + : channel->canBanMembers()) + && !_additional.isCreator(user) + && (!_additional.adminRights(user) + || _additional.canEditAdmin(user))) { + result.canRemove = true; + result.removeText = tr::lng_profile_kick(tr::now); + } + } break; + case Role::Admins: { + if (user + && !_additional.isCreator(user) + && _additional.adminRights(user).has_value() + && _additional.canEditAdmin(user)) { + result.canRemove = true; + result.removeText = tr::lng_profile_kick(tr::now); + } + } break; + case Role::Restricted: + case Role::Kicked: { + if (_additional.canRestrictParticipant(participant)) { + result.canRemove = true; + result.removeText = tr::lng_profile_delete_removed(tr::now); + } + } break; } + + if (user) { + const auto isSelf = user->isSelf(); + const auto canEditSelf = isSelf + && !_peer->amRestricted(ChatRestriction::EditRank); + const auto targetIsAdmin = + _additional.adminRights(user).has_value() + || _additional.isCreator(user); + const auto canEditTarget = !isSelf + && _peer->canManageRanks() + && (!targetIsAdmin || _additional.canEditAdmin(user)); + if (canEditSelf || canEditTarget) { + result.canEditTag = true; + } + if (isSelf + && result.rank.isEmpty() + && !result.canRemove + && result.rights == Rights::Normal + && canEditSelf) { + result.canAddTag = true; + } else if (canEditTarget && result.rank.isEmpty()) { + result.canAddTag = true; + } + } + return result; } void ParticipantsBoxController::recomputeTypeFor( not_null participant) { - if (_role != Role::Profile) { - return; - } const auto row = delegate()->peerListFindRow(participant->id.value); if (row) { static_cast(row)->setType(computeType(participant)); diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.h b/Telegram/SourceFiles/boxes/peers/edit_participants_box.h index bbd74b1a90..0a10b24538 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.h @@ -196,6 +196,9 @@ public: void prepare() override; void rowClicked(not_null row) override; void rowRightActionClicked(not_null row) override; + void rowElementClicked( + not_null row, + int element) override; base::unique_qptr rowContextMenu( QWidget *parent, not_null row) override; @@ -219,8 +222,10 @@ public: void setStoriesShown(bool shown); protected: - // Allow child controllers not providing navigation. - // This is their responsibility to override all methods that use it. + using Row = Info::Profile::MemberListRow; + using Type = Row::Type; + using Rights = Row::Rights; + struct CreateTag { }; ParticipantsBoxController( @@ -232,10 +237,10 @@ protected: virtual std::unique_ptr createRow( not_null participant) const; + std::unique_ptr _chatStyle; + mutable base::flat_map _pillCircleCache; + private: - using Row = Info::Profile::MemberListRow; - using Type = Row::Type; - using Rights = Row::Rights; struct SavedState : SavedStateBase { explicit SavedState(const ParticipantsAdditionalData &additional); @@ -321,9 +326,6 @@ private: std::unique_ptr _stories; - std::unique_ptr _chatStyle; - mutable base::flat_map _pillCircleCache; - }; // Members, banned and restricted users server side search. diff --git a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp index 5a81a58a20..2f22d81270 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_invite_controller.cpp @@ -747,7 +747,9 @@ std::unique_ptr InviteController::createRow( || user->isInaccessible()) { return nullptr; } - auto result = std::make_unique(user); + auto result = std::make_unique( + user, + Type{ .chatStyle = _chatStyle.get(), .circleCache = &_pillCircleCache }); _rowAdded.fire_copy(user); _inGroup.emplace(user); if (isAlreadyIn(user)) { diff --git a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway_list_controllers.cpp b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway_list_controllers.cpp index d060d7b9e8..15aa552e02 100644 --- a/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway_list_controllers.cpp +++ b/Telegram/SourceFiles/info/channel_statistics/boosts/giveaway/giveaway_list_controllers.cpp @@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/channel_statistics/boosts/giveaway/giveaway_list_controllers.h" #include "apiwrap.h" +#include "boxes/peer_list_controllers.h" #include "data/data_channel.h" #include "data/data_folder.h" #include "data/data_peer.h" @@ -138,7 +139,11 @@ std::unique_ptr AwardMembersListController::createRow( if (!user || user->isInaccessible() || user->isBot() || user->isSelf()) { return nullptr; } - return std::make_unique(participant); + auto type = Type{ + .chatStyle = _chatStyle.get(), + .circleCache = &_pillCircleCache, + }; + return std::make_unique(participant, type); } base::unique_qptr AwardMembersListController::rowContextMenu( diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index f2828617de..ca3bcd1885 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -1459,4 +1459,4 @@ earnTonIconMargin: margins(0px, 2px, 0px, 0px); infoMusicButtonRipple: universalRippleAnimation; infoMusicButtonPadding: margins(16px, 6px, 13px, 6px); -memberTagPillPadding: margins(5px, 2px, 5px, 2px); +memberTagPillPadding: margins(4px, 1px, 4px, 1px); diff --git a/Telegram/SourceFiles/info/profile/info_profile_members_controllers.cpp b/Telegram/SourceFiles/info/profile/info_profile_members_controllers.cpp index 895749d8d4..fd94b414c7 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_members_controllers.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_members_controllers.cpp @@ -25,9 +25,9 @@ namespace Info { namespace Profile { MemberListRow::MemberListRow( - not_null user, + not_null peer, Type type) -: PeerListRowWithLink(user) +: PeerListRow(peer) , _type(type) { setType(type); } @@ -36,87 +36,189 @@ MemberListRow::~MemberListRow() = default; void MemberListRow::setType(Type type) { _type = type; - _actionRipple = nullptr; - if (_type.canRemove) { - _tagMode = TagMode::Remove; - _actionText = tr::lng_profile_delete_removed(tr::now); - } else if (_type.canAddTag) { + _tagRipple = nullptr; + _removeRipple = nullptr; + if (_type.canAddTag) { _tagMode = TagMode::AddTag; - _actionText = tr::lng_context_add_my_tag(tr::now); + _tagText = tr::lng_context_add_my_tag(tr::now); } else if (!_type.rank.isEmpty()) { _tagMode = (_type.rights == Rights::Admin || _type.rights == Rights::Creator) ? TagMode::AdminPill : TagMode::NormalText; - _actionText = _type.rank; + _tagText = _type.rank; } else if (_type.rights == Rights::Creator) { _tagMode = TagMode::AdminPill; - _actionText = tr::lng_owner_badge(tr::now); + _tagText = tr::lng_owner_badge(tr::now); } else if (_type.rights == Rights::Admin) { _tagMode = TagMode::AdminPill; - _actionText = tr::lng_admin_badge(tr::now); + _tagText = tr::lng_admin_badge(tr::now); } else { _tagMode = TagMode::None; - _actionText = QString(); + _tagText = QString(); } - _actionTextWidth = _actionText.isEmpty() + _tagTextWidth = _tagText.isEmpty() ? 0 - : st::normalFont->width(_actionText); + : st::normalFont->width(_tagText); + if (_type.canRemove) { + _removeText = _type.removeText.isEmpty() + ? tr::lng_profile_kick(tr::now) + : _type.removeText; + _removeTextWidth = st::normalFont->width(_removeText); + } else { + _removeText = QString(); + _removeTextWidth = 0; + } } MemberListRow::Type MemberListRow::type() const { return _type; } -bool MemberListRow::rightActionDisabled() const { - if (_tagMode == TagMode::AddTag || _tagMode == TagMode::Remove) { - return false; - } - return !canRemove(); -} - -QSize MemberListRow::rightActionSize() const { - if (_actionTextWidth == 0) { - return QSize(); - } - switch (_tagMode) { - case TagMode::Remove: - case TagMode::AdminPill: - case TagMode::AddTag: { - const auto &p = st::memberTagPillPadding; - const auto h = p.top() + st::normalFont->height + p.bottom(); - const auto w = p.left() + _actionTextWidth + p.right(); - return QSize(std::max(w, h), h); - } - case TagMode::NormalText: - return QSize(_actionTextWidth, st::normalFont->height); - case TagMode::None: - return QSize(); - } - return QSize(); -} - -QMargins MemberListRow::rightActionMargins() const { - const auto skip = st::contactsCheckPosition.x(); - const auto &st = st::defaultPeerListItem; - const auto size = rightActionSize(); - if (size.isEmpty()) { - return QMargins(); - } - return QMargins( - skip, - (st.height - size.height()) / 2, - st.photoPosition.x() + skip, - 0); -} - -not_null MemberListRow::user() const { +UserData *MemberListRow::user() const { return peer()->asUser(); } +void MemberListRow::setRefreshCallback(Fn callback) { + _refreshCallback = std::move(callback); +} + +bool MemberListRow::tagInteractive() const { + return _type.canEditTag || _type.canAddTag; +} + +QSize MemberListRow::tagSize() const { + if (_tagTextWidth == 0) { + return QSize(); + } + const auto usePill = (_tagMode == TagMode::AdminPill) + || (_tagMode == TagMode::AddTag) + || ((_tagMode == TagMode::NormalText) && tagInteractive()); + if (usePill) { + const auto &p = st::memberTagPillPadding; + const auto h = p.top() + st::normalFont->height + p.bottom(); + const auto w = p.left() + _tagTextWidth + p.right(); + return QSize(std::max(w, h), h); + } + return QSize(_tagTextWidth, st::normalFont->height); +} + +QSize MemberListRow::removeSize() const { + if (_removeTextWidth == 0) { + return QSize(); + } + const auto &p = st::memberTagPillPadding; + const auto h = p.top() + st::normalFont->height + p.bottom(); + const auto w = p.left() + _removeTextWidth + p.right(); + return QSize(std::max(w, h), h); +} + +int MemberListRow::elementsCount() const { + return _type.canRemove ? 2 : 1; +} + +QRect MemberListRow::elementGeometry(int element, int outerWidth) const { + const auto skip = st::contactsCheckPosition.x(); + const auto &st = st::defaultPeerListItem; + const auto right = st.photoPosition.x() + skip; + + if (element == kTagElement) { + const auto size = tagSize(); + if (size.isEmpty()) { + return QRect(); + } + const auto left = outerWidth - right - size.width(); + if (!_type.canRemove) { + const auto top = (st.height - size.height()) / 2; + return QRect(QPoint(left, top), size); + } + const auto progress = _hoverAnimation.value( + _wasHovered ? 1. : 0.); + const auto centeredTop = (st.height - size.height()) / 2; + const auto statusAlignedTop = st.statusPosition.y() + + (st::contactsStatusFont->height - size.height()) / 2; + const auto top = anim::interpolate( + centeredTop, statusAlignedTop, progress); + return QRect(QPoint(left, top), size); + } else if (element == kRemoveElement) { + const auto size = removeSize(); + if (size.isEmpty()) { + return QRect(); + } + const auto left = outerWidth - right - size.width(); + const auto nameAlignedTop = st.namePosition.y() + + (st.nameStyle.font->height - size.height()) / 2; + return QRect(QPoint(left, nameAlignedTop), size); + } + return QRect(); +} + +bool MemberListRow::elementDisabled(int element) const { + if (element == kTagElement) { + return !tagInteractive(); + } + if (element == kRemoveElement) { + const auto progress = _hoverAnimation.value( + _wasHovered ? 1. : 0.); + return (progress == 0.); + } + return false; +} + +bool MemberListRow::elementOnlySelect(int element) const { + return false; +} + +void MemberListRow::elementAddRipple( + int element, + QPoint point, + Fn updateCallback) { + if (!_refreshCallback) { + _refreshCallback = updateCallback; + } + if (element == kTagElement) { + if (!tagInteractive()) { + return; + } + if (!_tagRipple) { + const auto size = tagSize(); + const auto radius = size.height() / 2; + auto mask = Ui::RippleAnimation::RoundRectMask( + size, radius); + _tagRipple = std::make_unique( + st::defaultLightButton.ripple, + std::move(mask), + updateCallback); + } + _tagRipple->add(point); + } else if (element == kRemoveElement) { + if (!_removeRipple) { + const auto size = removeSize(); + const auto radius = size.height() / 2; + auto mask = Ui::RippleAnimation::RoundRectMask( + size, radius); + _removeRipple = std::make_unique( + st::defaultLightButton.ripple, + std::move(mask), + updateCallback); + } + _removeRipple->add(point); + } +} + +void MemberListRow::elementsStopLastRipple() { + if (_tagRipple) { + _tagRipple->lastStop(); + } + if (_removeRipple) { + _removeRipple->lastStop(); + } +} + void MemberListRow::refreshStatus() { - if (user()->isBot()) { - const auto seesAllMessages = (user()->botInfo->readsAllHistory + const auto u = user(); + if (u && u->isBot()) { + const auto seesAllMessages = (u->botInfo->readsAllHistory || _type.rights != Rights::Normal); setCustomStatus(seesAllMessages ? tr::lng_status_bot_reads_all(tr::now) @@ -126,8 +228,16 @@ void MemberListRow::refreshStatus() { } } -bool MemberListRow::canRemove() const { - return _type.canRemove; +void MemberListRow::checkHoverChanged(bool hovered) { + if (!_type.canRemove || _wasHovered == hovered) { + return; + } + _wasHovered = hovered; + _hoverAnimation.start( + _refreshCallback ? _refreshCallback : Fn([] {}), + hovered ? 0. : 1., + hovered ? 1. : 0., + st::universalDuration); } int MemberListRow::pillHeight() const { @@ -187,95 +297,189 @@ void MemberListRow::paintPill( QRect(half * ratio, 0, otherHalf * ratio, h * ratio)); } -void MemberListRow::rightActionPaint( +void MemberListRow::paintColoredPill( Painter &p, int x, int y, + int w, + int textWidth, + const QString &text, + QColor color, + bool over, + std::unique_ptr &ripple, int outerWidth, - bool selected, - bool actionSelected) { - if (_actionTextWidth == 0) { + float64 baseAlpha, + float64 overAlpha) { + const auto &pad = st::memberTagPillPadding; + auto bgColor = color; + bgColor.setAlphaF(over ? overAlpha : baseAlpha); + paintPill(p, x, y, w, bgColor.rgba()); + if (ripple) { + auto rippleColor = color; + rippleColor.setAlphaF(0.12); + ripple->paint(p, x, y, outerWidth, &rippleColor); + if (ripple->empty()) { + ripple.reset(); + } + } + p.setFont(st::normalFont); + p.setPen(color); + p.drawTextLeft( + x + (w - textWidth) / 2, + y + pad.top(), + outerWidth, + text, + textWidth); +} + +void MemberListRow::paintTag( + Painter &p, + QRect geometry, + int outerWidth, + bool over) { + if (_tagTextWidth == 0) { return; } + const auto x = geometry.x(); + const auto y = geometry.y(); const auto &pad = st::memberTagPillPadding; switch (_tagMode) { case TagMode::AdminPill: { const auto nameColor = (_type.rights == Rights::Creator) ? st::rankOwnerFg->c : st::rankAdminFg->c; - auto bgColor = nameColor; - bgColor.setAlphaF(0.15); const auto h = pillHeight(); - const auto cw = pad.left() + _actionTextWidth + pad.right(); + const auto cw = pad.left() + _tagTextWidth + pad.right(); const auto w = std::max(cw, h); - paintPill(p, x, y, w, bgColor.rgba()); - p.setFont(st::normalFont); - p.setPen(nameColor); - p.drawTextLeft( - x + (w - _actionTextWidth) / 2, - y + pad.top(), - outerWidth, - _actionText, - _actionTextWidth); + paintColoredPill( + p, x, y, w, _tagTextWidth, _tagText, + nameColor, over, _tagRipple, outerWidth, + 0.15, 0.15); } break; case TagMode::NormalText: { - p.setFont(st::normalFont); - p.setPen(st::rankUserFg); - p.drawTextLeft( - x, y, outerWidth, _actionText, _actionTextWidth); + if (tagInteractive()) { + const auto h = pillHeight(); + const auto cw = pad.left() + _tagTextWidth + pad.right(); + const auto w = std::max(cw, h); + paintColoredPill( + p, x, y, w, _tagTextWidth, _tagText, + st::rankUserFg->c, over, _tagRipple, outerWidth); + } else { + p.setFont(st::normalFont); + p.setPen(st::rankUserFg); + p.drawTextLeft( + x, y, outerWidth, _tagText, _tagTextWidth); + } } break; - case TagMode::Remove: case TagMode::AddTag: { const auto h = pillHeight(); - const auto cw = pad.left() + _actionTextWidth + pad.right(); + const auto cw = pad.left() + _tagTextWidth + pad.right(); const auto w = std::max(cw, h); - if (actionSelected) { - paintPill(p, x, y, w, st::lightButtonBgOver->c.rgba()); - } - if (_actionRipple) { - const auto color = st::lightButtonBgRipple->c; - _actionRipple->paint(p, x, y, outerWidth, &color); - if (_actionRipple->empty()) { - _actionRipple.reset(); + const auto self = user() && user()->isSelf(); + if (self) { + if (over) { + paintPill( + p, x, y, w, st::lightButtonBgOver->c.rgba()); } + if (_tagRipple) { + const auto color = st::lightButtonBgRipple->c; + _tagRipple->paint(p, x, y, outerWidth, &color); + if (_tagRipple->empty()) { + _tagRipple.reset(); + } + } + p.setFont(st::normalFont); + p.setPen(over + ? st::lightButtonFgOver + : st::lightButtonFg); + } else { + paintColoredPill( + p, x, y, w, _tagTextWidth, _tagText, + st::rankUserFg->c, over, _tagRipple, outerWidth); + break; } - p.setFont(st::normalFont); - p.setPen(actionSelected - ? st::lightButtonFgOver - : st::lightButtonFg); p.drawTextLeft( - x + (w - _actionTextWidth) / 2, + x + (w - _tagTextWidth) / 2, y + pad.top(), outerWidth, - _actionText, - _actionTextWidth); + _tagText, + _tagTextWidth); } break; case TagMode::None: break; } } -void MemberListRow::rightActionAddRipple( - QPoint point, - Fn updateCallback) { - if (_tagMode != TagMode::AddTag && _tagMode != TagMode::Remove) { +void MemberListRow::paintRemove( + Painter &p, + QRect geometry, + int outerWidth, + bool over) { + if (_removeTextWidth == 0) { return; } - if (!_actionRipple) { - const auto size = rightActionSize(); - const auto radius = size.height() / 2; - auto mask = Ui::RippleAnimation::RoundRectMask(size, radius); - _actionRipple = std::make_unique( - st::defaultLightButton.ripple, - std::move(mask), - std::move(updateCallback)); + const auto progress = _hoverAnimation.value( + _wasHovered ? 1. : 0.); + if (progress == 0.) { + return; } - _actionRipple->add(point); + const auto x = geometry.x(); + const auto y = geometry.y(); + const auto &pad = st::memberTagPillPadding; + const auto h = pillHeight(); + const auto cw = pad.left() + _removeTextWidth + pad.right(); + const auto w = std::max(cw, h); + + auto o = p.opacity(); + p.setOpacity(o * progress); + + if (over) { + paintPill(p, x, y, w, st::lightButtonBgOver->c.rgba()); + } + if (_removeRipple) { + const auto color = st::lightButtonBgRipple->c; + _removeRipple->paint(p, x, y, outerWidth, &color); + if (_removeRipple->empty()) { + _removeRipple.reset(); + } + } + p.setFont(st::normalFont); + p.setPen(over ? st::lightButtonFgOver : st::lightButtonFg); + p.drawTextLeft( + x + (w - _removeTextWidth) / 2, + y + pad.top(), + outerWidth, + _removeText, + _removeTextWidth); + + p.setOpacity(o); } -void MemberListRow::rightActionStopLastRipple() { - if (_actionRipple) { - _actionRipple->lastStop(); +void MemberListRow::elementsPaint( + Painter &p, + int outerWidth, + bool selected, + int selectedElement) { + checkHoverChanged(selected || (selectedElement > 0)); + if (_type.canRemove) { + const auto removeGeometry = elementGeometry( + kRemoveElement, outerWidth); + if (!removeGeometry.isEmpty()) { + paintRemove( + p, + removeGeometry, + outerWidth, + (selectedElement == kRemoveElement)); + } + } + const auto tagGeometry = elementGeometry( + kTagElement, outerWidth); + if (!tagGeometry.isEmpty()) { + paintTag( + p, + tagGeometry, + outerWidth, + (selectedElement == kTagElement)); } } diff --git a/Telegram/SourceFiles/info/profile/info_profile_members_controllers.h b/Telegram/SourceFiles/info/profile/info_profile_members_controllers.h index 055b7051e7..d05fd348f1 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_members_controllers.h +++ b/Telegram/SourceFiles/info/profile/info_profile_members_controllers.h @@ -7,8 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once -#include "boxes/peer_list_controllers.h" +#include "boxes/peer_list_box.h" #include "base/flat_map.h" +#include "ui/effects/animations.h" #include "ui/unread_badge.h" namespace Ui { @@ -24,7 +25,7 @@ class SessionNavigation; namespace Info { namespace Profile { -class MemberListRow final : public PeerListRowWithLink { +class MemberListRow final : public PeerListRow { public: enum class Rights { Normal, @@ -34,45 +35,51 @@ public: struct Type { Rights rights; QString rank; + QString removeText; not_null chatStyle; not_null*> circleCache; bool canAddTag = false; + bool canEditTag = false; bool canRemove = false; }; - MemberListRow(not_null user, Type type); + static constexpr auto kTagElement = 1; + static constexpr auto kRemoveElement = 2; + + MemberListRow(not_null peer, Type type); ~MemberListRow(); void setType(Type type); [[nodiscard]] Type type() const; - bool rightActionDisabled() const override; - QSize rightActionSize() const override; - QMargins rightActionMargins() const override; - void rightActionPaint( - Painter &p, - int x, - int y, - int outerWidth, - bool selected, - bool actionSelected) override; - void rightActionAddRipple( - QPoint point, - Fn updateCallback) override; - void rightActionStopLastRipple() override; + void setRefreshCallback(Fn callback); void refreshStatus() override; - not_null user() const; + [[nodiscard]] UserData *user() const; + + int elementsCount() const override; + QRect elementGeometry(int element, int outerWidth) const override; + bool elementDisabled(int element) const override; + bool elementOnlySelect(int element) const override; + void elementAddRipple( + int element, + QPoint point, + Fn updateCallback) override; + void elementsStopLastRipple() override; + void elementsPaint( + Painter &p, + int outerWidth, + bool selected, + int selectedElement) override; private: enum class TagMode { None, - Remove, AdminPill, NormalText, AddTag, }; - [[nodiscard]] bool canRemove() const; + [[nodiscard]] bool tagInteractive() const; [[nodiscard]] int pillHeight() const; [[nodiscard]] const QImage &ensurePillCircle(QRgb color) const; void paintPill( @@ -81,12 +88,44 @@ private: int y, int width, QRgb bgColor) const; + void paintColoredPill( + Painter &p, + int x, + int y, + int w, + int textWidth, + const QString &text, + QColor color, + bool over, + std::unique_ptr &ripple, + int outerWidth, + float64 baseAlpha = 0.12, + float64 overAlpha = 0.24); + void paintTag( + Painter &p, + QRect geometry, + int outerWidth, + bool over); + void paintRemove( + Painter &p, + QRect geometry, + int outerWidth, + bool over); + [[nodiscard]] QSize tagSize() const; + [[nodiscard]] QSize removeSize() const; + void checkHoverChanged(bool hovered); Type _type; TagMode _tagMode = TagMode::None; - QString _actionText; - int _actionTextWidth = 0; - std::unique_ptr _actionRipple; + QString _tagText; + int _tagTextWidth = 0; + QString _removeText; + int _removeTextWidth = 0; + Ui::Animations::Simple _hoverAnimation; + std::unique_ptr _tagRipple; + std::unique_ptr _removeRipple; + Fn _refreshCallback; + bool _wasHovered = false; }; diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 0963c42ea3..03cd8da385 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -23,7 +23,7 @@ msgMinWidth: 160px; msgPhotoSize: 33px; msgPhotoSkip: 40px; msgPadding: margins(11px, 8px, 11px, 8px); -msgTagBadgePadding: margins(5px, 1px, 5px, 1px); +msgTagBadgePadding: margins(4px, 0px, 4px, 0px); msgTagBadgeBoostSkip: 2px; msgMargin: margins(16px, 6px, 56px, 2px); msgMarginTopAttached: 0px;