From c39c0b597cdb7155dcee534f20eb50f16d4368f0 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 23 Feb 2026 22:15:26 +0400 Subject: [PATCH] Nice tag badge click handler in groups. --- Telegram/CMakeLists.txt | 2 + .../Resources/icons/chat/large_user_tag.svg | 7 + Telegram/Resources/langs/lang.strings | 14 + .../SourceFiles/api/api_chat_participants.cpp | 25 +- .../boxes/peers/edit_participants_box.cpp | 3 +- .../SourceFiles/boxes/peers/tag_info_box.cpp | 531 ++++++++++++++++++ .../SourceFiles/boxes/peers/tag_info_box.h | 27 + .../SourceFiles/data/data_channel_admins.cpp | 12 +- .../SourceFiles/data/data_channel_admins.h | 1 + .../view/history_view_chat_section.cpp | 6 + .../history/view/history_view_message.cpp | 67 ++- .../history/view/history_view_message.h | 1 + Telegram/SourceFiles/ui/chat/chat.style | 24 + 13 files changed, 689 insertions(+), 31 deletions(-) create mode 100644 Telegram/Resources/icons/chat/large_user_tag.svg create mode 100644 Telegram/SourceFiles/boxes/peers/tag_info_box.cpp create mode 100644 Telegram/SourceFiles/boxes/peers/tag_info_box.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 0bcb434fdd..389bdb6fcd 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -257,6 +257,8 @@ PRIVATE boxes/peers/prepare_short_info_box.h boxes/peers/replace_boost_box.cpp boxes/peers/replace_boost_box.h + boxes/peers/tag_info_box.cpp + boxes/peers/tag_info_box.h boxes/peers/verify_peers_box.cpp boxes/peers/verify_peers_box.h boxes/about_box.cpp diff --git a/Telegram/Resources/icons/chat/large_user_tag.svg b/Telegram/Resources/icons/chat/large_user_tag.svg new file mode 100644 index 0000000000..ed97783eba --- /dev/null +++ b/Telegram/Resources/icons/chat/large_user_tag.svg @@ -0,0 +1,7 @@ + + + Other / Large / large_user_tag + + + + \ No newline at end of file diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index a6d0a69823..72296389fd 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -6021,6 +6021,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "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_tag_info_title_user" = "Member Tags"; +"lng_tag_info_title_admin" = "Admin Tags"; +"lng_tag_info_title_owner" = "Owner Tags"; +"lng_tag_info_text_user" = "This grey tag {emoji} is {author}'s member tag in {group}."; +"lng_tag_info_text_admin" = "This green tag {emoji} is {author}'s admin tag in {group}."; +"lng_tag_info_text_owner" = "This purple tag {emoji} is {author}'s owner tag in {group}."; +"lng_tag_info_preview_member" = "Member Tag"; +"lng_tag_info_preview_admin" = "Admin Tag"; +"lng_tag_info_preview_owner" = "Owner Tag"; +"lng_tag_info_add_my_tag" = "Add My Tag"; +"lng_tag_info_edit_my_tag" = "Edit My Tag"; +"lng_tag_info_admins_only" = "Only admins can change tags in this group."; + "lng_rights_promote_member" = "Promote to Admin"; "lng_rights_remove_member" = "Remove from Group"; "lng_rights_dismiss_admin" = "Dismiss Admin"; diff --git a/Telegram/SourceFiles/api/api_chat_participants.cpp b/Telegram/SourceFiles/api/api_chat_participants.cpp index 904306929b..8900ca8dba 100644 --- a/Telegram/SourceFiles/api/api_chat_participants.cpp +++ b/Telegram/SourceFiles/api/api_chat_participants.cpp @@ -51,12 +51,9 @@ std::vector ParseList( void ApplyMegagroupAdmins(not_null channel, Members list) { Expects(channel->isMegagroup()); - const auto i = ranges::find_if(list, &Api::ChatParticipant::isCreator); - if (i != list.end()) { - i->tryApplyCreatorTo(channel); - } else { - channel->mgInfo->creator = nullptr; - } + const auto creatorIt = ranges::find_if( + list, + &Api::ChatParticipant::isCreator); auto adding = base::flat_set(); auto addingRanks = base::flat_map(); @@ -68,13 +65,10 @@ void ApplyMegagroupAdmins(not_null channel, Members list) { } } } - if (channel->mgInfo->creator) { - 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); + if (creatorIt != list.end() && creatorIt->isUser()) { + adding.emplace(creatorIt->userId()); + if (!creatorIt->rank().isEmpty()) { + addingRanks.emplace(creatorIt->userId(), creatorIt->rank()); } } auto removing = channel->mgInfo->admins; @@ -84,6 +78,11 @@ void ApplyMegagroupAdmins(not_null channel, Members list) { } Data::ChannelAdminChanges changes(channel); + if (creatorIt != list.end()) { + creatorIt->tryApplyCreatorTo(channel); + } else { + channel->mgInfo->creator = nullptr; + } for (const auto &addingId : adding) { const auto r = addingRanks.find(addingId); const auto rank = (r != end(addingRanks)) diff --git a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp index 3a5a276690..f88326378f 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_participants_box.cpp @@ -2080,8 +2080,7 @@ void ParticipantsBoxController::editRestrictedDone( if (_role == Role::Restricted) { prependRow(participant); } else if (_role == Role::Kicked - || _role == Role::Admins - || _role == Role::Members) { + || _role == Role::Admins) { removeRow(participant); } } diff --git a/Telegram/SourceFiles/boxes/peers/tag_info_box.cpp b/Telegram/SourceFiles/boxes/peers/tag_info_box.cpp new file mode 100644 index 0000000000..53b699a963 --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/tag_info_box.cpp @@ -0,0 +1,531 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "boxes/peers/tag_info_box.h" + +#include "boxes/peers/edit_participants_box.h" +#include "boxes/peers/edit_tag_control.h" +#include "data/data_channel.h" +#include "data/data_chat.h" +#include "data/data_chat_participant_status.h" +#include "data/data_peer.h" +#include "data/data_user.h" +#include "history/view/history_view_message.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "ui/chat/chat_style.h" +#include "ui/chat/chat_theme.h" +#include "ui/layers/generic_box.h" +#include "ui/painter.h" +#include "ui/text/custom_emoji_helper.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "window/section_widget.h" +#include "window/themes/window_theme.h" +#include "styles/style_boxes.h" +#include "styles/style_calls.h" +#include "styles/style_chat.h" +#include "styles/style_info.h" +#include "styles/style_layers.h" +#include "styles/style_widgets.h" + +namespace { + +using HistoryView::BadgeRole; + +constexpr auto kTextLinesAlpha = 0.1; + +[[nodiscard]] QColor RoleColor(BadgeRole role) { + return (role == BadgeRole::Creator) + ? st::rankOwnerFg->c + : (role == BadgeRole::Admin) + ? st::rankAdminFg->c + : st::rankUserFg->c; +} + +[[nodiscard]] object_ptr MakeRoundColoredLogo( + not_null parent, + BadgeRole role) { + const auto &icon = st::tagInfoIcon; + const auto &padding = st::tagInfoIconPadding; + const auto logoSize = icon.size(); + const auto logoOuter = logoSize.grownBy(padding); + auto result = object_ptr(parent); + const auto logo = result.data(); + logo->resize(logo->width(), logoOuter.height()); + logo->paintRequest() | rpl::on_next([=, &icon] { + if (logo->width() < logoOuter.width()) { + return; + } + auto p = QPainter(logo); + auto hq = PainterHighQualityEnabler(p); + const auto x = (logo->width() - logoOuter.width()) / 2; + const auto outer = QRect(QPoint(x, 0), logoOuter); + p.setBrush(RoleColor(role)); + p.setPen(Qt::NoPen); + p.drawEllipse(outer); + icon.paintInCenter(p, outer); + }, logo->lifetime()); + return result; +} + +[[nodiscard]] Ui::Text::PaletteDependentEmoji MakeTagPillEmoji( + const QString &text, + BadgeRole role) { + return { .factory = [=] { + const auto color = RoleColor(role); + const auto &padding = st::msgTagBadgePadding; + auto string = Ui::Text::String(st::defaultTextStyle, text); + const auto textWidth = string.maxWidth(); + const auto isUser = (role == BadgeRole::User); + const auto contentWidth = padding.left() + + textWidth + + padding.right(); + const auto pillHeight = padding.top() + + st::msgFont->height + + padding.bottom(); + const auto imgWidth = isUser + ? textWidth + : std::max(contentWidth, pillHeight); + const auto imgHeight = isUser + ? st::msgFont->height + : pillHeight; + const auto ratio = style::DevicePixelRatio(); + + auto result = QImage( + QSize(imgWidth, imgHeight) * ratio, + QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(ratio); + result.fill(Qt::transparent); + + auto p = QPainter(&result); + auto hq = PainterHighQualityEnabler(p); + if (!isUser) { + auto bgColor = color; + bgColor.setAlphaF(0.15); + p.setPen(Qt::NoPen); + p.setBrush(bgColor); + p.drawRoundedRect( + 0, + 0, + imgWidth, + imgHeight, + imgHeight / 2., + imgHeight / 2.); + } + p.setPen(color); + string.draw(p, { + .position = QPoint( + isUser ? 0 : ((imgWidth - textWidth) / 2), + isUser ? 0 : padding.top()), + .availableWidth = textWidth, + }); + p.end(); + return result; + }, .margin = st::customEmojiTextBadgeMargin }; +} + +class TagPreviewsWidget final : public Ui::RpWidget { +public: + TagPreviewsWidget( + QWidget *parent, + not_null session, + BadgeRole role); + +protected: + void paintEvent(QPaintEvent *e) override; + +private: + void paintPreview( + QPainter &p, + QRect rect, + BadgeRole previewRole) const; + void paintBubbleToImage( + QRect rect, + BadgeRole previewRole) const; + void invalidateCache(); + + const std::unique_ptr _theme; + std::unique_ptr _style; + BadgeRole _role = BadgeRole::User; + mutable QImage _leftCache; + mutable QImage _rightCache; + +}; + +TagPreviewsWidget::TagPreviewsWidget( + QWidget *parent, + not_null session, + BadgeRole role) +: RpWidget(parent) +, _theme(Window::Theme::DefaultChatThemeOn(lifetime())) +, _style(std::make_unique( + session->colorIndicesValue())) +, _role(role) { + _style->apply(_theme.get()); + resize(width(), st::tagInfoPreviewHeight); + + _theme->repaintBackgroundRequests( + ) | rpl::on_next([=] { + invalidateCache(); + update(); + }, lifetime()); + + style::PaletteChanged() | rpl::on_next([=] { + invalidateCache(); + update(); + }, lifetime()); + + sizeValue() | rpl::skip(1) | rpl::on_next([=] { + invalidateCache(); + }, lifetime()); +} + +void TagPreviewsWidget::invalidateCache() { + _leftCache = QImage(); + _rightCache = QImage(); +} + +void TagPreviewsWidget::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + + const auto gap = st::tagInfoPreviewGap; + const auto previewWidth = (width() - gap) / 2; + if (previewWidth <= 0) { + return; + } + + const auto leftRect = QRect(0, 0, previewWidth, height()); + const auto rightRect = QRect( + previewWidth + gap, + 0, + width() - previewWidth - gap, + height()); + + paintPreview(p, leftRect, BadgeRole::User); + + const auto rightRole = (_role == BadgeRole::User) + ? BadgeRole::Admin + : _role; + paintPreview(p, rightRect, rightRole); +} + +void TagPreviewsWidget::paintPreview( + QPainter &p, + QRect rect, + BadgeRole previewRole) const { + const auto previewRadius = st::tagInfoPreviewRadius; + + p.save(); + p.translate(rect.topLeft()); + + const auto local = QRect(0, 0, rect.width(), rect.height()); + auto clipPath = QPainterPath(); + clipPath.addRoundedRect(local, previewRadius, previewRadius); + p.setClipPath(clipPath); + + Window::SectionWidget::PaintBackground( + p, + _theme.get(), + QSize(rect.width(), window()->height()), + local); + + auto &cache = (previewRole == BadgeRole::User) + ? _leftCache + : _rightCache; + if (cache.isNull()) { + paintBubbleToImage(rect, previewRole); + } + p.drawImage(0, 0, cache); + + p.restore(); +} + +void TagPreviewsWidget::paintBubbleToImage( + QRect rect, + BadgeRole previewRole) const { + const auto ratio = style::DevicePixelRatio(); + auto &cache = (previewRole == BadgeRole::User) + ? _leftCache + : _rightCache; + cache = QImage( + rect.size() * ratio, + QImage::Format_ARGB32_Premultiplied); + cache.setDevicePixelRatio(ratio); + cache.fill(Qt::transparent); + + const auto &stm = _style->messageStyle(false, false); + const auto &padding = st::tagInfoPreviewBubblePadding; + const auto radius = st::tagInfoPreviewBubbleRadius; + const auto rightMargin = st::tagInfoPreviewBubbleRightMargin; + const auto bubbleTop = padding.top(); + const auto bubbleHeight = rect.height() + - padding.top() + - padding.bottom(); + const auto bubbleRight = rect.width() - rightMargin; + const auto bubbleRect = QRect( + -radius, + bubbleTop, + bubbleRight + radius, + bubbleHeight); + + auto p = QPainter(&cache); + { + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(stm.msgBg); + p.drawRoundedRect(bubbleRect, radius, radius); + } + + const auto innerLeft = padding.left(); + const auto innerRight = bubbleRight - padding.right(); + const auto available = innerRight - innerLeft; + + const auto badgeColor = RoleColor(previewRole); + const auto badgeText = (previewRole == BadgeRole::Creator) + ? tr::lng_tag_info_preview_owner(tr::now) + : (previewRole == BadgeRole::Admin) + ? tr::lng_tag_info_preview_admin(tr::now) + : tr::lng_tag_info_preview_member(tr::now); + auto badgeString = Ui::Text::String(st::defaultTextStyle, badgeText); + const auto badgeTextWidth = badgeString.maxWidth(); + const auto &badgePadding = st::msgTagBadgePadding; + const auto badgeContentWidth = badgePadding.left() + + badgeTextWidth + + badgePadding.right(); + const auto pillHeight = badgePadding.top() + + st::msgFont->height + + badgePadding.bottom(); + const auto pillWidth = std::max(badgeContentWidth, pillHeight); + + const auto badgeRight = innerRight; + const auto badgeLeft = badgeRight - pillWidth; + const auto badgeTop = bubbleTop + st::tagInfoPreviewBadgeTop; + + if (previewRole != BadgeRole::User) { + auto bgColor = badgeColor; + bgColor.setAlphaF(0.15); + const auto pillRect = QRect( + badgeLeft, + badgeTop, + pillWidth, + pillHeight); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(bgColor); + p.drawRoundedRect(pillRect, pillHeight / 2., pillHeight / 2.); + p.setPen(badgeColor); + badgeString.draw(p, { + .position = QPoint( + badgeLeft + (pillWidth - badgeTextWidth) / 2, + badgeTop + badgePadding.top()), + .availableWidth = badgeTextWidth, + }); + } else if (badgeTextWidth > 0) { + p.setPen(st::rankUserFg); + badgeString.draw(p, { + .position = QPoint(innerRight - badgeTextWidth, badgeTop), + .availableWidth = badgeTextWidth, + }); + } + + p.setFont(st::msgDateFont); + const auto timeText = u"12:00"_q; + const auto timeWidth = st::msgDateFont->width(timeText); + const auto timeX = innerRight - timeWidth; + const auto timeY = bubbleTop + + bubbleHeight + - padding.bottom() + + st::msgDateFont->ascent + - st::msgDateFont->height; + p.setPen(stm.msgDateFg); + p.drawText(timeX, timeY, timeText); + + { + auto color = stm.historyTextFg->c; + color.setAlphaF(color.alphaF() * kTextLinesAlpha); + p.setPen(Qt::NoPen); + p.setBrush(color); + auto hq = PainterHighQualityEnabler(p); + + const auto lineHeight = st::tagInfoPreviewLineHeight; + const auto lineSpacing = st::tagInfoPreviewLineSpacing; + const auto linesTop = badgeTop + + pillHeight + + lineSpacing; + const auto lineRadius = lineHeight / 2.0; + const auto timeAreaLeft = timeX - padding.right(); + const auto fractions = { 1.0, 0.65, 0.65 }; + auto y = double(linesTop); + auto lineIndex = 0; + for (const auto fraction : fractions) { + auto w = available * fraction; + const auto lineBottom = y + lineHeight; + if (lineIndex >= 1 && lineBottom > (timeY - lineSpacing)) { + w = std::min(w, double(timeAreaLeft - innerLeft)); + } + p.drawRoundedRect( + QRectF(innerLeft, y, w, lineHeight), + lineRadius, + lineRadius); + y += lineHeight + lineSpacing; + ++lineIndex; + } + } + + const auto fadeWidth = st::tagInfoPreviewFadeWidth; + p.setCompositionMode(QPainter::CompositionMode_DestinationIn); + auto gradient = QLinearGradient(0, 0, fadeWidth, 0); + gradient.setStops({ + { 0., QColor(255, 255, 255, 0) }, + { 1., QColor(255, 255, 255, 255) }, + }); + p.fillRect(0, 0, fadeWidth, rect.height(), gradient); + p.end(); +} + +[[nodiscard]] QString LookupCurrentRank(not_null peer) { + const auto selfId = peerToUser(peer->session().user()->id); + if (const auto channel = peer->asMegagroup()) { + if (const auto info = channel->mgInfo.get()) { + const auto it = info->memberRanks.find(selfId); + if (it != info->memberRanks.end()) { + return it->second; + } + } + } else if (const auto chat = peer->asChat()) { + const auto it = chat->memberRanks.find(selfId); + if (it != chat->memberRanks.end()) { + return it->second; + } + } + return QString(); +} + +} // namespace + +void TagInfoBox( + not_null box, + std::shared_ptr show, + not_null peer, + not_null author, + const QString &tagText, + HistoryView::BadgeRole role) { + box->setStyle(st::confcallJoinBox); + box->setWidth(st::boxWideWidth); + box->setNoContentMargin(true); + box->addTopButton(st::boxTitleClose, [=] { + box->closeBox(); + }); + + box->addRow( + MakeRoundColoredLogo(box, role), + st::boxRowPadding + st::confcallLinkHeaderIconPadding); + + auto title = (role == BadgeRole::Creator) + ? tr::lng_tag_info_title_owner() + : (role == BadgeRole::Admin) + ? tr::lng_tag_info_title_admin() + : tr::lng_tag_info_title_user(); + box->addRow( + object_ptr(box, std::move(title), st::boxTitle), + st::boxRowPadding + st::confcallLinkTitlePadding, + style::al_top); + + auto helper = Ui::Text::CustomEmojiHelper(); + const auto tagPill = helper.paletteDependent( + MakeTagPillEmoji(tagText, role)); + const auto authorName = author->shortName(); + const auto groupName = peer->name(); + const auto descText = (role == BadgeRole::Creator) + ? tr::lng_tag_info_text_owner( + tr::now, + lt_emoji, + tagPill, + lt_author, + tr::bold(authorName), + lt_group, + tr::bold(groupName), + tr::rich) + : (role == BadgeRole::Admin) + ? tr::lng_tag_info_text_admin( + tr::now, + lt_emoji, + tagPill, + lt_author, + tr::bold(authorName), + lt_group, + tr::bold(groupName), + tr::rich) + : tr::lng_tag_info_text_user( + tr::now, + lt_emoji, + tagPill, + lt_author, + tr::bold(authorName), + lt_group, + tr::bold(groupName), + tr::rich); + const auto context = helper.context(); + const auto desc = box->addRow( + object_ptr( + box, + rpl::single(descText), + st::confcallLinkCenteredText, + st::defaultPopupMenu, + context), + st::boxRowPadding, + style::al_top); + desc->setTryMakeSimilarLines(true); + + box->addRow( + object_ptr( + box, + &peer->session(), + role), + st::boxRowPadding + st::tagInfoPreviewPadding); + + const auto selfUser = peer->session().user(); + const auto selfRole = LookupBadgeRole(peer, selfUser); + const auto isAdmin = (selfRole != BadgeRole::User); + const auto canEditSelf = isAdmin + || !peer->amRestricted(ChatRestriction::EditRank); + + if (canEditSelf) { + const auto currentRank = LookupCurrentRank(peer); + auto buttonText = currentRank.isEmpty() + ? tr::lng_tag_info_add_my_tag() + : tr::lng_tag_info_edit_my_tag(); + box->addButton(std::move(buttonText), [=] { + box->closeBox(); + show->show(Box( + EditCustomRankBox, + show, + peer, + selfUser, + currentRank, + true, + Fn(nullptr))); + }); + } else { + box->addRow( + object_ptr( + box, + tr::lng_tag_info_admins_only(), + st::tagInfoAdminsOnlyLabel), + st::boxRowPadding + st::tagInfoAdminsOnlyPadding, + style::al_top); + box->addButton( + rpl::single(QString()), + [=] { box->closeBox(); } + )->setText(rpl::single(Ui::Text::IconEmoji( + &st::infoStarsUnderstood + ).append(' ').append( + tr::lng_stars_rating_understood(tr::now)))); + } +} diff --git a/Telegram/SourceFiles/boxes/peers/tag_info_box.h b/Telegram/SourceFiles/boxes/peers/tag_info_box.h new file mode 100644 index 0000000000..f54813eef0 --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/tag_info_box.h @@ -0,0 +1,27 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Ui { +class GenericBox; +class Show; +} // namespace Ui + +namespace HistoryView { +enum class BadgeRole : uchar; +} // namespace HistoryView + +class PeerData; + +void TagInfoBox( + not_null box, + std::shared_ptr show, + not_null peer, + not_null author, + const QString &tagText, + HistoryView::BadgeRole role); diff --git a/Telegram/SourceFiles/data/data_channel_admins.cpp b/Telegram/SourceFiles/data/data_channel_admins.cpp index 157c532be1..49764162cc 100644 --- a/Telegram/SourceFiles/data/data_channel_admins.cpp +++ b/Telegram/SourceFiles/data/data_channel_admins.cpp @@ -17,7 +17,8 @@ namespace Data { ChannelAdminChanges::ChannelAdminChanges(not_null channel) : _channel(channel) -, _admins(_channel->mgInfo->admins) { +, _admins(_channel->mgInfo->admins) +, _oldCreator(_channel->mgInfo->creator) { } void ChannelAdminChanges::add(UserId userId, const QString &rank) { @@ -45,6 +46,15 @@ void ChannelAdminChanges::remove(UserId userId) { } ChannelAdminChanges::~ChannelAdminChanges() { + const auto creator = _channel->mgInfo->creator; + if (creator != _oldCreator) { + if (creator) { + _changes.emplace(peerToUser(creator->id)); + } + if (_oldCreator) { + _changes.emplace(peerToUser(_oldCreator->id)); + } + } if (_changes.size() > 1 || (!_changes.empty() && _changes.front() != _channel->session().userId())) { diff --git a/Telegram/SourceFiles/data/data_channel_admins.h b/Telegram/SourceFiles/data/data_channel_admins.h index b37f67d6ff..b97d3259f3 100644 --- a/Telegram/SourceFiles/data/data_channel_admins.h +++ b/Telegram/SourceFiles/data/data_channel_admins.h @@ -22,6 +22,7 @@ private: not_null _channel; base::flat_set &_admins; base::flat_set _changes; + UserData *_oldCreator = nullptr; }; diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 10d809ff1c..8a8f8ac224 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -41,6 +41,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/ui_utility.h" #include "base/timer_rpl.h" #include "api/api_bot.h" +#include "api/api_chat_participants.h" #include "api/api_editing.h" #include "api/api_sending.h" #include "apiwrap.h" @@ -299,6 +300,11 @@ ChatWidget::ChatWidget( setupTranslateBar(); _peer->updateFull(); + if (const auto channel = _peer->asMegagroup()) { + if (!channel->mgInfo->adminsLoaded) { + session().api().chatParticipants().requestAdmins(channel); + } + } refreshTopBarActiveChat(); diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 6ffb0ec71b..61f5bc2f03 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "boxes/premium_preview_box.h" #include "boxes/share_box.h" +#include "boxes/peers/tag_info_box.h" #include "ui/effects/reaction_fly_animation.h" #include "ui/text/text_utilities.h" #include "ui/text/text_extended_data.h" @@ -248,20 +249,27 @@ void Message::refreshRightBadge() { if (const auto badge = Get(); badge && badge->overridden) { return; } + if (hasOutLayout()) { + if (Has()) { + RemoveComponents(RightBadge::Bit()); + } + return; + } const auto item = data(); - const auto [text, role] = [&]() -> std::pair { + const auto [text, role, special] = [&]() -> std::tuple { if (item->isDiscussionPost()) { return { (delegate()->elementContext() == Context::Replies) ? QString() : tr::lng_channel_badge(tr::now), BadgeRole::User, + true, }; } else if (item->author()->isMegagroup()) { if (const auto msgsigned = item->Get()) { if (!msgsigned->viaBusinessBot) { Assert(msgsigned->isAnonymousRank); - return { msgsigned->author, BadgeRole::User }; + return { msgsigned->author, BadgeRole::User, false }; } } } @@ -279,14 +287,14 @@ void Message::refreshRightBadge() { : chat->admins.contains(user) ? BadgeRole::Admin : BadgeRole::User; - return { j->second, basicRole }; + return { j->second, basicRole, false }; } } } - return { QString(), BadgeRole::User }; + return { QString(), BadgeRole::User, false }; } if (!user) { - return { QString(), BadgeRole::User }; + return { QString(), BadgeRole::User, false }; } const auto info = channel->mgInfo.get(); const auto userId = peerToUser(user->id); @@ -298,18 +306,19 @@ void Message::refreshRightBadge() { return { r->second, isCreator ? BadgeRole::Creator : BadgeRole::Admin, + false, }; } if (isCreator) { - return { tr::lng_owner_badge(tr::now), BadgeRole::Creator }; + return { tr::lng_owner_badge(tr::now), BadgeRole::Creator, false }; } - return { tr::lng_admin_badge(tr::now), BadgeRole::Admin }; + return { tr::lng_admin_badge(tr::now), BadgeRole::Admin, false }; } const auto fromRank = item->fromRank(); if (!fromRank.isEmpty()) { - return { fromRank, BadgeRole::User }; + return { fromRank, BadgeRole::User, false }; } - return { QString(), BadgeRole::User }; + return { QString(), BadgeRole::User, false }; }(); auto tagText = TextWithEntities{ (text.isEmpty() @@ -329,6 +338,8 @@ void Message::refreshRightBadge() { } const auto badge = Get(); badge->role = role; + badge->special = special || (text.isEmpty() && !tagText.empty()); + badge->tagLink = nullptr; if (tagText.empty()) { badge->tag.clear(); } else { @@ -2811,10 +2822,16 @@ bool Message::getStateFromName( && point.x() >= boostLeft && point.x() < badgeRight) { if (!badge->boostsLink) { - badge->boostsLink = std::make_shared([=]( - ClickContext context) { + const auto fullId = item->fullId(); + badge->boostsLink = std::make_shared([ + fullId + ](ClickContext context) { if (const auto controller = ExtractController(context)) { - controller->showToast(u"Boosts clicked"_q); + if (const auto item = controller->session().data().message(fullId)) { + if (const auto channel = item->history()->peer->asChannel()) { + controller->resolveBoostState(channel); + } + } } }); } @@ -2825,11 +2842,31 @@ bool Message::getStateFromName( ? (boostLeft - st::msgTagBadgeBoostSkip) : badgeRight; if (point.x() >= badgeLeft && point.x() < tagRight) { + if (badge->special) { + return false; + } if (!badge->tagLink) { - badge->tagLink = std::make_shared([=]( - ClickContext context) { + const auto weak = base::make_weak(this); + badge->tagLink = std::make_shared([ + weak + ](ClickContext context) { if (const auto controller = ExtractController(context)) { - controller->showToast(u"Tag clicked"_q); + if (const auto view = weak.get()) { + const auto badge = view->Get(); + if (!badge) { + return; + } + const auto item = view->data(); + const auto peer = item->history()->peer; + const auto author = item->author(); + controller->uiShow()->show(Box( + TagInfoBox, + controller->uiShow(), + peer, + author, + badge->tag.toString(), + badge->role)); + } } }); } diff --git a/Telegram/SourceFiles/history/view/history_view_message.h b/Telegram/SourceFiles/history/view/history_view_message.h index d7768b2086..8f69238af7 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.h +++ b/Telegram/SourceFiles/history/view/history_view_message.h @@ -77,6 +77,7 @@ struct RightBadge : RuntimeComponent { int width = 0; BadgeRole role = BadgeRole::User; bool overridden = false; + bool special = false; }; struct BottomRippleMask { diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index c983f8cf90..6a42ed7b89 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -25,6 +25,30 @@ msgPhotoSkip: 40px; msgPadding: margins(11px, 8px, 11px, 8px); msgTagBadgePadding: margins(5px, -1px, 5px, 0px); msgTagBadgeBoostSkip: 2px; + +tagInfoIcon: icon {{ "chat/large_user_tag-48x48", windowFgActive }}; +tagInfoIconPadding: margins(8px, 8px, 8px, 8px); +tagInfoPreviewHeight: 80px; +tagInfoPreviewGap: 8px; +tagInfoPreviewBubbleRadius: 8px; +tagInfoPreviewLineHeight: 6px; +tagInfoPreviewLineSpacing: 4px; +tagInfoPreviewBubblePadding: margins(10px, 8px, 10px, 8px); +tagInfoPreviewRadius: 8px; +tagInfoPreviewBubbleRightMargin: 10px; +tagInfoPreviewFadeWidth: 24px; +tagInfoPreviewBadgeTop: 6px; +tagInfoPreviewPadding: margins(0px, 16px, 0px, 0px); +tagInfoAdminsOnlyPadding: margins(0px, 8px, 0px, 0px); +tagInfoAdminsOnlyLabel: FlatLabel(defaultFlatLabel) { + align: align(top); + minWidth: 40px; + textFg: windowSubTextFg; + style: TextStyle(defaultTextStyle) { + font: font(12px); + } +} + msgMargin: margins(16px, 6px, 56px, 2px); msgMarginTopAttached: 0px; msgShadow: 2px;