diff --git a/Telegram/SourceFiles/api/api_chat_participants.cpp b/Telegram/SourceFiles/api/api_chat_participants.cpp index a1d0a8cb94..155b326716 100644 --- a/Telegram/SourceFiles/api/api_chat_participants.cpp +++ b/Telegram/SourceFiles/api/api_chat_participants.cpp @@ -56,35 +56,43 @@ void ApplyMegagroupAdmins(not_null channel, Members list) { i->tryApplyCreatorTo(channel); } else { channel->mgInfo->creator = nullptr; - channel->mgInfo->creatorRank = QString(); } - auto adding = base::flat_map(); + auto adding = base::flat_set(); + auto addingRanks = base::flat_map(); for (const auto &p : list) { if (p.isUser()) { - adding.emplace(p.userId(), p.rank()); + adding.emplace(p.userId()); + if (!p.rank().isEmpty()) { + addingRanks.emplace(p.userId(), p.rank()); + } } } if (channel->mgInfo->creator) { - adding.emplace( - peerToUser(channel->mgInfo->creator->id), - channel->mgInfo->creatorRank); + const auto creatorId = peerToUser(channel->mgInfo->creator->id); + adding.emplace(creatorId); + const auto r = channel->mgInfo->memberRanks.find(creatorId); + if (r != channel->mgInfo->memberRanks.end() + && !r->second.isEmpty()) { + addingRanks.emplace(creatorId, r->second); + } } auto removing = channel->mgInfo->admins; if (removing.empty() && adding.empty()) { - // Add some admin-placeholder so we don't DDOS - // server with admins list requests. LOG(("API Error: Got empty admins list from server.")); - adding.emplace(0, QString()); + adding.emplace(UserId(0)); } Data::ChannelAdminChanges changes(channel); - for (const auto &[addingId, rank] : adding) { - if (!removing.remove(addingId)) { - changes.add(addingId, rank); - } + for (const auto &addingId : adding) { + const auto r = addingRanks.find(addingId); + const auto rank = (r != end(addingRanks)) + ? r->second + : QString(); + removing.remove(addingId); + changes.add(addingId, rank); } - for (const auto &[removingId, rank] : removing) { + for (const auto &removingId : removing) { changes.remove(removingId); } } @@ -149,7 +157,7 @@ void ApplyLastList( channel->mgInfo->botStatus = 2; } } - if (!p.rank().isEmpty() && !p.isCreatorOrAdmin()) { + if (!p.rank().isEmpty()) { channel->mgInfo->memberRanks[p.userId()] = p.rank(); } } @@ -337,7 +345,11 @@ void ChatParticipant::tryApplyCreatorTo( if (isCreator() && isUser()) { if (const auto info = channel->mgInfo.get()) { info->creator = channel->owner().userLoaded(userId()); - info->creatorRank = rank(); + if (!rank().isEmpty()) { + info->memberRanks[userId()] = rank(); + } else { + info->memberRanks.remove(userId()); + } } } } diff --git a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp index 5355138ac8..018ea98b65 100644 --- a/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/add_participants_box.cpp @@ -1500,7 +1500,7 @@ void AddSpecialBoxController::showAdmin( _peer, user, currentRights, - _additional.adminRank(user), + _additional.memberRank(user), _additional.adminPromotedSince(user), _additional.adminPromotedBy(user)); const auto show = delegate()->peerListUiShow(); diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp index dc66c5453c..2a1df5285f 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp @@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "data/data_changes.h" #include "base/unixtime.h" +#include "ui/chat/chat_style.h" #include "ui/effects/outline_segments.h" #include "ui/layers/generic_box.h" #include "ui/widgets/fields/input_field.h" @@ -428,12 +429,6 @@ auto ParticipantsAdditionalData::adminRights( : std::nullopt; } -QString ParticipantsAdditionalData::adminRank( - not_null user) const { - const auto i = _adminRanks.find(user); - return (i != end(_adminRanks)) ? i->second : QString(); -} - QString ParticipantsAdditionalData::memberRank( not_null user) const { const auto i = _memberRanks.find(user); @@ -511,7 +506,6 @@ void ParticipantsAdditionalData::setExternal( _adminRights.erase(user); _adminCanEdit.erase(user); _adminPromotedBy.erase(user); - _adminRanks.erase(user); _admins.erase(user); } _restrictedRights.erase(participant); @@ -565,11 +559,10 @@ void ParticipantsAdditionalData::fillFromChannel( } if (information->creator) { _creator = information->creator; - _adminRanks[information->creator] = information->creatorRank; } for (const auto user : information->lastParticipants) { const auto admin = information->lastAdmins.find(user); - const auto rank = information->admins.find(peerToUser(user->id)); + const auto rank = information->memberRanks.find(peerToUser(user->id)); const auto restricted = information->lastRestricted.find(user); if (admin != information->lastAdmins.cend()) { _restrictedRights.erase(user); @@ -581,15 +574,14 @@ void ParticipantsAdditionalData::fillFromChannel( _adminCanEdit.erase(user); } _adminRights.emplace(user, admin->second.rights); - if (rank != end(information->admins) + if (rank != end(information->memberRanks) && !rank->second.isEmpty()) { - _adminRanks[user] = rank->second; + _memberRanks[user] = rank->second; } } else if (restricted != information->lastRestricted.cend()) { _adminRights.erase(user); _adminCanEdit.erase(user); _adminPromotedBy.erase(user); - _adminRanks.erase(user); _restrictedRights.emplace(user, restricted->second.rights); } } @@ -727,7 +719,7 @@ PeerData *ParticipantsAdditionalData::applyParticipant( Unexpected("Api::ChatParticipant::type in applyParticipant."); }(); if (const auto user = result ? result->asUser() : nullptr) { - if (!data.rank().isEmpty() && !data.isCreatorOrAdmin()) { + if (!data.rank().isEmpty()) { _memberRanks[user] = data.rank(); } else { _memberRanks.remove(user); @@ -746,11 +738,6 @@ UserData *ParticipantsAdditionalData::applyCreator( } else { _adminCanEdit.erase(user); } - if (!data.rank().isEmpty()) { - _adminRanks[user] = data.rank(); - } else { - _adminRanks.remove(user); - } return user; } return nullptr; @@ -777,11 +764,6 @@ UserData *ParticipantsAdditionalData::applyAdmin( } else { _adminCanEdit.erase(user); } - if (!data.rank().isEmpty()) { - _adminRanks[user] = data.rank(); - } else { - _adminRanks.remove(user); - } if (data.promotedSince()) { _adminPromotedSince[user] = data.promotedSince(); } else { @@ -815,7 +797,6 @@ UserData *ParticipantsAdditionalData::applyRegular(UserId userId) { _adminRights.erase(user); _adminCanEdit.erase(user); _adminPromotedBy.erase(user); - _adminRanks.erase(user); _restrictedRights.erase(user); _kicked.erase(user); _restrictedBy.erase(user); @@ -834,7 +815,6 @@ PeerData *ParticipantsAdditionalData::applyBanned( _adminRights.erase(user); _adminCanEdit.erase(user); _adminPromotedBy.erase(user); - _adminRanks.erase(user); } if (data.isKicked()) { _kicked.emplace(participant); @@ -949,6 +929,8 @@ ParticipantsBoxController::ParticipantsBoxController( : ParticipantsBoxController(CreateTag(), navigation, peer, role) { } +ParticipantsBoxController::~ParticipantsBoxController() = default; + ParticipantsBoxController::ParticipantsBoxController( CreateTag, Window::SessionNavigation *navigation, @@ -959,7 +941,9 @@ ParticipantsBoxController::ParticipantsBoxController( , _peer(peer) , _api(&_peer->session().mtp()) , _role(role) -, _additional(peer, _role) { +, _additional(peer, _role) +, _chatStyle( + std::make_unique(peer->session().colorIndicesValue())) { subscribeToMigration(); if (_role == Role::Profile) { setupListChangeViewers(); @@ -1381,6 +1365,10 @@ void ParticipantsBoxController::prepare() { recomputeTypeFor(update.user); refreshRows(); }, lifetime()); + + style::PaletteChanged() | rpl::on_next([=] { + _pillCircleCache.clear(); + }, lifetime()); } void ParticipantsBoxController::unload() { @@ -1741,6 +1729,26 @@ void ParticipantsBoxController::rowRightActionClicked( 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; + } + } kickParticipant(participant); } else if (_role == Role::Admins) { Assert(user != nullptr); @@ -1921,7 +1929,7 @@ void ParticipantsBoxController::showAdmin(not_null user) { _peer, user, currentRights, - _additional.adminRank(user), + _additional.memberRank(user), _additional.adminPromotedSince(user), _additional.adminPromotedBy(user)); if (_additional.canAddOrEditAdmin(user)) { @@ -2245,7 +2253,10 @@ std::unique_ptr ParticipantsBoxController::createRow( auto ParticipantsBoxController::computeType( not_null participant) const -> Type { const auto user = participant->asUser(); - auto result = Type(); + auto result = Type{ + .chatStyle = _chatStyle.get(), + .circleCache = &_pillCircleCache, + }; result.rights = (user && _additional.isCreator(user)) ? Rights::Creator : (user && _additional.adminRights(user).has_value()) @@ -2255,10 +2266,15 @@ auto ParticipantsBoxController::computeType( && user && user->isInaccessible(); if (!result.canRemove) { - const auto aRank = user ? _additional.adminRank(user) : QString(); - result.adminRank = !aRank.isEmpty() - ? aRank - : (user ? _additional.memberRank(user) : QString()); + result.rank = user ? _additional.memberRank(user) : QString(); + } + if (user + && user->isSelf() + && result.rank.isEmpty() + && !result.canRemove + && result.rights == Rights::Normal + && !_peer->amRestricted(ChatRestriction::EditRank)) { + result.canAddTag = true; } return result; } @@ -2271,6 +2287,7 @@ void ParticipantsBoxController::recomputeTypeFor( const auto row = delegate()->peerListFindRow(participant->id.value); if (row) { static_cast(row)->setType(computeType(participant)); + delegate()->peerListUpdateRow(row); } } diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.h b/Telegram/SourceFiles/boxes/peers/edit_participants_box.h index 8bfa406f84..bbd74b1a90 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.h +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.h @@ -115,7 +115,6 @@ public: not_null participant) const; [[nodiscard]] std::optional adminRights( not_null user) const; - [[nodiscard]] QString adminRank(not_null user) const; [[nodiscard]] QString memberRank(not_null user) const; [[nodiscard]] std::optional restrictedRights( not_null participant) const; @@ -161,7 +160,6 @@ private: // Data for channels. base::flat_map, ChatAdminRightsInfo> _adminRights; - base::flat_map, QString> _adminRanks; base::flat_map, QString> _memberRanks; base::flat_map, TimeId> _adminPromotedSince; base::flat_map, TimeId> _restrictedSince; @@ -192,6 +190,7 @@ public: not_null navigation, not_null peer, Role role); + ~ParticipantsBoxController(); Main::Session &session() const override; void prepare() override; @@ -322,6 +321,9 @@ 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/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp index b80e9c354a..905957f084 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp @@ -516,7 +516,7 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) { _channel->session().api().chatParticipants().requestAdmins(_channel); } else { mrows.reserve(mrows.size() + _channel->mgInfo->admins.size()); - for (const auto &[userId, rank] : _channel->mgInfo->admins) { + for (const auto &userId : _channel->mgInfo->admins) { if (const auto user = _channel->owner().userLoaded(userId)) { if (user->isInaccessible()) continue; if (!listAllSuggestions && filterNotPassedByName(user)) continue; diff --git a/Telegram/SourceFiles/data/data_channel.h b/Telegram/SourceFiles/data/data_channel.h index 259900ff60..38078150a6 100644 --- a/Telegram/SourceFiles/data/data_channel.h +++ b/Telegram/SourceFiles/data/data_channel.h @@ -141,12 +141,10 @@ public: base::flat_set> bots; rpl::event_stream unrestrictedByBoostsChanges; - // For admin badges, full admins list with ranks. - base::flat_map admins; + base::flat_set admins; base::flat_map memberRanks; UserData *creator = nullptr; // nullptr means unknown - QString creatorRank; int botStatus = 0; // -1 - no bots, 0 - unknown, 1 - one bot, that sees all history, 2 - other bool joinedMessageFound = false; bool adminsLoaded = false; diff --git a/Telegram/SourceFiles/data/data_channel_admins.cpp b/Telegram/SourceFiles/data/data_channel_admins.cpp index 6dd286573e..a14b548c52 100644 --- a/Telegram/SourceFiles/data/data_channel_admins.cpp +++ b/Telegram/SourceFiles/data/data_channel_admins.cpp @@ -20,16 +20,25 @@ ChannelAdminChanges::ChannelAdminChanges(not_null channel) } void ChannelAdminChanges::add(UserId userId, const QString &rank) { - const auto i = _admins.find(userId); - if (i == end(_admins) || i->second != rank) { - _admins[userId] = rank; + if (_admins.emplace(userId).second) { _changes.emplace(userId); } + auto &ranks = _channel->mgInfo->memberRanks; + if (!rank.isEmpty()) { + const auto i = ranks.find(userId); + if (i == end(ranks) || i->second != rank) { + ranks[userId] = rank; + _changes.emplace(userId); + } + } else { + if (ranks.remove(userId)) { + _changes.emplace(userId); + } + } } void ChannelAdminChanges::remove(UserId userId) { - if (_admins.contains(userId)) { - _admins.remove(userId); + if (_admins.remove(userId)) { _changes.emplace(userId); } } diff --git a/Telegram/SourceFiles/data/data_channel_admins.h b/Telegram/SourceFiles/data/data_channel_admins.h index b069e57a49..b37f67d6ff 100644 --- a/Telegram/SourceFiles/data/data_channel_admins.h +++ b/Telegram/SourceFiles/data/data_channel_admins.h @@ -20,7 +20,7 @@ public: private: not_null _channel; - base::flat_map &_admins; + base::flat_set &_admins; base::flat_set _changes; }; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 2d7e3ec04b..41266d6f97 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -258,16 +258,19 @@ void Message::initPaidInformation() { void Message::refreshRightBadge() { const auto item = data(); - const auto text = [&] { + const auto [text, isAdmin] = [&]() -> std::pair { if (item->isDiscussionPost()) { - return (delegate()->elementContext() == Context::Replies) - ? QString() - : tr::lng_channel_badge(tr::now); + return { + (delegate()->elementContext() == Context::Replies) + ? QString() + : tr::lng_channel_badge(tr::now), + false, + }; } else if (item->author()->isMegagroup()) { if (const auto msgsigned = item->Get()) { if (!msgsigned->viaBusinessBot) { Assert(msgsigned->isAnonymousRank); - return msgsigned->author; + return { msgsigned->author, false }; } } } @@ -279,47 +282,43 @@ void Message::refreshRightBadge() { const auto j = chat->memberRanks.find( peerToUser(user->id)); if (j != chat->memberRanks.end()) { - return j->second; + return { j->second, false }; } } } - return QString(); + return { QString(), false }; } if (!user) { - return QString(); + return { QString(), false }; } const auto info = channel->mgInfo.get(); 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 + const auto isAdmin = info->admins.contains(userId); + const auto r = info->memberRanks.find(userId); + const auto custom = (r != info->memberRanks.end()) + ? r->second : QString(); if (!custom.isEmpty()) { - return custom; + return { custom, isAdmin || info->creator == user }; } if (info->creator == user) { - return tr::lng_owner_badge(tr::now); + return { tr::lng_owner_badge(tr::now), true }; } - if (i != info->admins.end()) { - return tr::lng_admin_badge(tr::now); + if (isAdmin) { + return { tr::lng_admin_badge(tr::now), true }; } const auto fromRank = item->fromRank(); if (!fromRank.isEmpty()) { - return fromRank; + return { fromRank, false }; } - const auto j = info->memberRanks.find(userId); - if (j != info->memberRanks.end()) { - return j->second; - } - return QString(); + return { QString(), false }; }(); auto badge = TextWithEntities{ (text.isEmpty() ? delegate()->elementAuthorRank(this) : TextUtilities::RemoveEmoji(TextUtilities::SingleLine(text))) }; + _rightBadgeIsAdmin = isAdmin ? 1 : 0; _rightBadgeHasBoosts = 0; if (const auto boosts = item->boostsApplied()) { _rightBadgeHasBoosts = 1; @@ -341,6 +340,20 @@ void Message::refreshRightBadge() { } } +int Message::rightBadgeWidth() const { + if (!_rightBadgeIsAdmin) { + return _rightBadge.maxWidth(); + } + const auto &padding = st::msgTagBadgePadding; + const auto textWidth = padding.left() + + _rightBadge.maxWidth() + + padding.right(); + const auto pillHeight = padding.top() + + st::msgFont->height + + padding.bottom(); + return std::max(textWidth, pillHeight); +} + void Message::applyGroupAdminChanges( const base::flat_set &changes) { if (!data()->out() @@ -713,9 +726,8 @@ QSize Message::performCountOptimalSize() { ? st::msgFont->width(FastReplyText()) : 0; if (!_rightBadge.isEmpty()) { - const auto badgeWidth = _rightBadge.maxWidth(); namew += st::msgPadding.right() - + std::max(badgeWidth, replyWidth); + + std::max(rightBadgeWidth(), replyWidth); } else if (replyWidth) { namew += st::msgPadding.right() + replyWidth; } @@ -1602,7 +1614,9 @@ void Message::paintFromName( if (!displayFromName()) { return; } - const auto badgeWidth = _rightBadge.isEmpty() ? 0 : _rightBadge.maxWidth(); + const auto badgeWidth = _rightBadge.isEmpty() + ? 0 + : rightBadgeWidth(); const auto replyWidth = [&] { if (isUnderCursor()) { if (displayFastForward()) { @@ -1710,6 +1724,56 @@ void Message::paintFromName( trect.left() + trect.width() - rightWidth, trect.top() + st::msgFont->ascent, hasFastForward() ? FastForwardText() : FastReplyText()); + } else if (_rightBadgeIsAdmin) { + const auto nameColor = FromNameFg( + context, + colorIndex(), + colorCollectible()); + auto bgColor = nameColor; + bgColor.setAlphaF(0.15); + const auto &padding = st::msgTagBadgePadding; + const auto textWidth = _rightBadge.maxWidth(); + const auto contentWidth = padding.left() + + textWidth + + padding.right(); + const auto pillHeight = padding.top() + + st::msgFont->height + + padding.bottom(); + const auto totalWidth = std::max(contentWidth, pillHeight); + const auto badgeLeft = trect.left() + + trect.width() + - totalWidth; + const auto badgeTop = trect.top() + + (st::msgNameFont->height - pillHeight) / 2; + const auto pillRect = QRect( + badgeLeft, + badgeTop, + totalWidth, + pillHeight); + p.setPen(Qt::NoPen); + p.setBrush(bgColor); + { + auto hq = PainterHighQualityEnabler(p); + p.drawRoundedRect( + pillRect, + pillHeight / 2., + pillHeight / 2.); + } + p.setPen(nameColor); + const auto boostPen = !_rightBadgeHasBoosts + ? QPen() + : QPen(nameColor); + auto colored = std::array{ + { { &boostPen, &boostPen } }, + }; + _rightBadge.draw(p, { + .position = QPoint( + badgeLeft + (totalWidth - textWidth) / 2, + badgeTop + padding.top()), + .availableWidth = textWidth, + .colors = colored, + .now = context.now, + }); } else { const auto shift = QPoint(trect.width() - rightWidth, 0); const auto pen = !_rightBadgeHasBoosts @@ -4195,8 +4259,8 @@ void Message::fromNameUpdated(int width) const { ? st::msgFont->width(FastReplyText()) : 0; if (!_rightBadge.isEmpty()) { - const auto badgeWidth = _rightBadge.maxWidth(); - width -= st::msgPadding.right() + std::max(badgeWidth, replyWidth); + width -= st::msgPadding.right() + + std::max(rightBadgeWidth(), replyWidth); } else if (replyWidth) { width -= st::msgPadding.right() + replyWidth; } diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index 0f1f11da16..b4e04c936d 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -323,6 +323,7 @@ private: void psaTooltipToggled(bool shown) const; void refreshRightBadge(); + [[nodiscard]] int rightBadgeWidth() const; void validateFromNameText(PeerData *from) const; void ensureFromNameStatusLink(not_null peer) const; @@ -338,10 +339,11 @@ private: mutable std::unique_ptr _selectionRoundCheckbox; Ui::Text::String _rightBadge; mutable int _fromNameVersion = 0; - uint32 _bubbleWidthLimit : 28 = 0; + uint32 _bubbleWidthLimit : 27 = 0; uint32 _invertMedia : 1 = 0; uint32 _hideReply : 1 = 0; uint32 _rightBadgeHasBoosts : 1 = 0; + uint32 _rightBadgeIsAdmin : 1 = 0; uint32 _postShowingAuthor : 1 = 0; BottomInfo _bottomInfo; diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index 49da24caf3..f2828617de 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -1458,3 +1458,5 @@ earnTonIconMargin: margins(0px, 2px, 0px, 0px); infoMusicButtonRipple: universalRippleAnimation; infoMusicButtonPadding: margins(16px, 6px, 13px, 6px); + +memberTagPillPadding: margins(5px, 2px, 5px, 2px); diff --git a/Telegram/SourceFiles/info/profile/info_profile_members_controllers.cpp b/Telegram/SourceFiles/info/profile/info_profile_members_controllers.cpp index ff5e942166..908210632b 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_members_controllers.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_members_controllers.cpp @@ -13,9 +13,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_user.h" #include "ui/unread_badge.h" #include "lang/lang_keys.h" +#include "ui/chat/chat_style.h" +#include "ui/effects/ripple_animation.h" +#include "ui/painter.h" #include "styles/style_info.h" #include "styles/style_boxes.h" +#include "styles/style_chat.h" #include "styles/style_dialogs.h" +#include "styles/style_widgets.h" namespace Info { namespace Profile { @@ -28,17 +33,36 @@ MemberListRow::MemberListRow( setType(type); } +MemberListRow::~MemberListRow() = default; + void MemberListRow::setType(Type type) { _type = type; - PeerListRowWithLink::setActionLink(_type.canRemove - ? tr::lng_profile_delete_removed(tr::now) - : !_type.adminRank.isEmpty() - ? _type.adminRank - : (_type.rights == Rights::Creator) - ? tr::lng_owner_badge(tr::now) - : (_type.rights == Rights::Admin) - ? tr::lng_admin_badge(tr::now) - : QString()); + _actionRipple = nullptr; + if (_type.canRemove) { + _tagMode = TagMode::Remove; + _actionText = tr::lng_profile_delete_removed(tr::now); + } else if (_type.canAddTag) { + _tagMode = TagMode::AddTag; + _actionText = 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; + } else if (_type.rights == Rights::Creator) { + _tagMode = TagMode::AdminPill; + _actionText = tr::lng_owner_badge(tr::now); + } else if (_type.rights == Rights::Admin) { + _tagMode = TagMode::AdminPill; + _actionText = tr::lng_admin_badge(tr::now); + } else { + _tagMode = TagMode::None; + _actionText = QString(); + } + _actionTextWidth = _actionText.isEmpty() + ? 0 + : st::normalFont->width(_actionText); } MemberListRow::Type MemberListRow::type() const { @@ -46,23 +70,44 @@ MemberListRow::Type MemberListRow::type() const { } 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(); - if (canRemove()) { - const auto &st = st::defaultPeerListItem; - return QMargins( - skip, - (st.height - st.nameStyle.font->height) / 2, - st.photoPosition.x() + skip, - 0); + const auto &st = st::defaultPeerListItem; + const auto size = rightActionSize(); + if (size.isEmpty()) { + return QMargins(); } return QMargins( skip, - st::defaultPeerListItem.namePosition.y(), - st::defaultPeerListItem.photoPosition.x() + skip, + (st.height - size.height()) / 2, + st.photoPosition.x() + skip, 0); } @@ -86,6 +131,157 @@ bool MemberListRow::canRemove() const { return _type.canRemove; } +int MemberListRow::pillHeight() const { + const auto &p = st::memberTagPillPadding; + return p.top() + st::normalFont->height + p.bottom(); +} + +const QImage &MemberListRow::ensurePillCircle(QRgb color) const { + auto &cache = *_type.circleCache; + const auto it = cache.find(color); + if (it != end(cache)) { + return it->second; + } + const auto h = pillHeight(); + const auto ratio = style::DevicePixelRatio(); + auto image = QImage( + QSize(h, h) * ratio, + QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(ratio); + image.fill(Qt::transparent); + { + auto p = QPainter(&image); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(QColor::fromRgba(color)); + p.drawEllipse(0, 0, h, h); + } + return cache.emplace(color, std::move(image)).first->second; +} + +void MemberListRow::paintPill( + Painter &p, + int x, + int y, + int width, + QRgb bgColor) const { + const auto h = pillHeight(); + const auto &circle = ensurePillCircle(bgColor); + const auto ratio = style::DevicePixelRatio(); + const auto half = h / 2; + const auto otherHalf = h - half; + p.drawImage( + QRect(x, y, half, h), + circle, + QRect(0, 0, half * ratio, h * ratio)); + if (width > h) { + p.fillRect( + x + half, + y, + width - h, + h, + QColor::fromRgba(bgColor)); + } + p.drawImage( + QRect(x + width - otherHalf, y, otherHalf, h), + circle, + QRect(half * ratio, 0, otherHalf * ratio, h * ratio)); +} + +void MemberListRow::rightActionPaint( + Painter &p, + int x, + int y, + int outerWidth, + bool selected, + bool actionSelected) { + if (_actionTextWidth == 0) { + return; + } + const auto &pad = st::memberTagPillPadding; + switch (_tagMode) { + case TagMode::AdminPill: { + const auto nameColor = Ui::FromNameFg( + _type.chatStyle, + false, + user()->colorIndex(), + user()->colorCollectible()); + auto bgColor = nameColor; + bgColor.setAlphaF(0.15); + const auto h = pillHeight(); + const auto cw = pad.left() + _actionTextWidth + 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); + } break; + case TagMode::NormalText: { + p.setFont(st::normalFont); + p.setPen(st::windowSubTextFg); + p.drawTextLeft( + x, y, outerWidth, _actionText, _actionTextWidth); + } break; + case TagMode::Remove: + case TagMode::AddTag: { + const auto h = pillHeight(); + const auto cw = pad.left() + _actionTextWidth + 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(); + } + } + p.setFont(st::normalFont); + p.setPen(actionSelected + ? st::lightButtonFgOver + : st::lightButtonFg); + p.drawTextLeft( + x + (w - _actionTextWidth) / 2, + y + pad.top(), + outerWidth, + _actionText, + _actionTextWidth); + } break; + case TagMode::None: + break; + } +} + +void MemberListRow::rightActionAddRipple( + QPoint point, + Fn updateCallback) { + if (_tagMode != TagMode::AddTag && _tagMode != TagMode::Remove) { + 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)); + } + _actionRipple->add(point); +} + +void MemberListRow::rightActionStopLastRipple() { + if (_actionRipple) { + _actionRipple->lastStop(); + } +} + std::unique_ptr CreateMembersController( not_null navigation, not_null peer) { diff --git a/Telegram/SourceFiles/info/profile/info_profile_members_controllers.h b/Telegram/SourceFiles/info/profile/info_profile_members_controllers.h index c1c0b2f9cc..055b7051e7 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_members_controllers.h +++ b/Telegram/SourceFiles/info/profile/info_profile_members_controllers.h @@ -8,8 +8,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "boxes/peer_list_controllers.h" +#include "base/flat_map.h" #include "ui/unread_badge.h" +namespace Ui { +class ChatStyle; +} // namespace Ui + class ParticipantsBoxController; namespace Window { @@ -28,23 +33,60 @@ public: }; struct Type { Rights rights; + QString rank; + not_null chatStyle; + not_null*> circleCache; + bool canAddTag = false; bool canRemove = false; - QString adminRank; }; MemberListRow(not_null user, 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 refreshStatus() override; not_null user() const; private: + enum class TagMode { + None, + Remove, + AdminPill, + NormalText, + AddTag, + }; + [[nodiscard]] bool canRemove() const; + [[nodiscard]] int pillHeight() const; + [[nodiscard]] const QImage &ensurePillCircle(QRgb color) const; + void paintPill( + Painter &p, + int x, + int y, + int width, + QRgb bgColor) const; + Type _type; + TagMode _tagMode = TagMode::None; + QString _actionText; + int _actionTextWidth = 0; + std::unique_ptr _actionRipple; }; diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index 5c697f6bfd..abb09bf92d 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -23,6 +23,7 @@ msgMinWidth: 160px; msgPhotoSize: 33px; msgPhotoSkip: 40px; msgPadding: margins(11px, 8px, 11px, 8px); +msgTagBadgePadding: margins(5px, 1px, 5px, 1px); msgMargin: margins(16px, 6px, 56px, 2px); msgMarginTopAttached: 0px; msgShadow: 2px;