Show similar channels under join message.

This commit is contained in:
John Preston 2023-11-21 13:31:38 +04:00
parent 91fba41e2c
commit 36a8c49213
30 changed files with 814 additions and 74 deletions

View file

@ -713,6 +713,8 @@ PRIVATE
history/view/media/history_view_premium_gift.h
history/view/media/history_view_service_box.cpp
history/view/media/history_view_service_box.h
history/view/media/history_view_similar_channels.cpp
history/view/media/history_view_similar_channels.h
history/view/media/history_view_slot_machine.cpp
history/view/media/history_view_slot_machine.h
history/view/media/history_view_sticker.cpp

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 B

View file

@ -1678,6 +1678,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_action_giveaway_results_some" = "Some winners of the giveaway was randomly selected by Telegram and received private messages with giftcodes.";
"lng_action_giveaway_results_none" = "No winners of the giveaway could be selected.";
"lng_similar_channels_title" = "Similar channels";
"lng_similar_channels_view_all" = "View all";
"lng_premium_gift_duration_months#one" = "for {count} month";
"lng_premium_gift_duration_months#other" = "for {count} months";
"lng_premium_gift_duration_years#one" = "for {count} year";

View file

@ -211,6 +211,23 @@ void ApplyBotsList(
Data::PeerUpdate::Flag::FullInfo);
}
[[nodiscard]] std::vector<not_null<ChannelData*>> ParseSimilar(
not_null<ChannelData*> channel,
const MTPmessages_Chats &chats) {
auto result = std::vector<not_null<ChannelData*>>();
chats.match([&](const auto &data) {
const auto &list = data.vchats().v;
result.reserve(list.size());
for (const auto &chat : list) {
const auto peer = channel->owner().processChat(chat);
if (const auto channel = peer->asChannel()) {
result.push_back(channel);
}
}
});
return result;
}
} // namespace
ChatParticipant::ChatParticipant(
@ -559,6 +576,7 @@ void ChatParticipants::requestSelf(not_null<ChannelData*> channel) {
UserId inviter = -1,
TimeId inviteDate = 0,
bool inviteViaRequest = false) {
const auto dateChanged = (channel->inviteDate != inviteDate);
channel->inviter = inviter;
channel->inviteDate = inviteDate;
channel->inviteViaRequest = inviteViaRequest;
@ -569,6 +587,9 @@ void ChatParticipants::requestSelf(not_null<ChannelData*> channel) {
} else {
history->owner().histories().requestDialogEntry(history);
}
if (dateChanged) {
loadSimilarChannels(channel);
}
}
};
_selfParticipantRequests.emplace(channel);
@ -685,4 +706,35 @@ void ChatParticipants::unblock(
_kickRequests.emplace(kick, requestId);
}
void ChatParticipants::loadSimilarChannels(not_null<ChannelData*> channel) {
if (!channel->isBroadcast() || _similar.contains(channel)) {
return;
}
_similar[channel].requestId = _api.request(
MTPchannels_GetChannelRecommendations(channel->inputChannel)
).done([=](const MTPmessages_Chats &result) {
_similar[channel] = {
.list = ParseSimilar(channel, result),
};
_similarLoaded.fire_copy(channel);
}).send();
}
const std::vector<not_null<ChannelData*>> &ChatParticipants::similar(
not_null<ChannelData*> channel) {
const auto i = channel->isBroadcast()
? _similar.find(channel)
: end(_similar);
if (i != end(_similar)) {
return i->second.list;
}
static const auto empty = std::vector<not_null<ChannelData*>>();
return empty;
}
auto ChatParticipants::similarLoaded() const
-> rpl::producer<not_null<ChannelData*>> {
return _similarLoaded.events();
}
} // namespace Api

View file

@ -120,7 +120,19 @@ public:
not_null<ChannelData*> channel,
not_null<PeerData*> participant);
[[nodiscard]] const std::vector<not_null<ChannelData*>> &similar(
not_null<ChannelData*> channel);
[[nodiscard]] auto similarLoaded() const
-> rpl::producer<not_null<ChannelData*>>;
private:
struct SimilarChannels {
std::vector<not_null<ChannelData*>> list;
mtpRequestId requestId = 0;
};
void loadSimilarChannels(not_null<ChannelData*> channel);
MTP::Sender _api;
using PeerRequests = base::flat_map<PeerData*, mtpRequestId>;
@ -143,6 +155,9 @@ private:
not_null<PeerData*>>;
base::flat_map<KickRequest, mtpRequestId> _kickRequests;
base::flat_map<not_null<ChannelData*>, SimilarChannels> _similar;
rpl::event_stream<not_null<ChannelData*>> _similarLoaded;
};
} // namespace Api

View file

@ -309,6 +309,8 @@ enum class MessageFlag : uint64 {
// If not set then we need to refresh _displayFrom value.
DisplayFromChecked = (1ULL << 40),
ShowSimilarChannels = (1ULL << 41),
};
inline constexpr bool is_flag_type(MessageFlag) { return true; }
using MessageFlags = base::flags<MessageFlag>;

View file

@ -37,6 +37,7 @@ namespace {
constexpr auto kTopLayer = 2;
constexpr auto kBottomLayer = 1;
constexpr auto kNoneLayer = 0;
constexpr auto kBlurRadius = 24;
[[nodiscard]] QImage CornerBadgeTTL(
not_null<PeerData*> peer,
@ -46,38 +47,17 @@ constexpr auto kNoneLayer = 0;
if (!ttl) {
return QImage();
}
constexpr auto kBlurRadius = 24;
const auto ratio = style::DevicePixelRatio();
const auto fullSize = photoSize;
const auto blurredFull = Images::BlurLargeImage(
peer->generateUserpicImage(view, fullSize * ratio, 0),
kBlurRadius);
const auto partRect = CornerBadgeTTLRect(fullSize);
const auto &partSize = partRect.width();
auto result = [&] {
auto blurredPart = blurredFull.copy(
blurredFull.width() - partSize * ratio,
blurredFull.height() - partSize * ratio,
partSize * ratio,
partSize * ratio);
blurredPart.setDevicePixelRatio(ratio);
constexpr auto kMinAcceptableContrast = 4.5;
const auto averageColor = Ui::CountAverageColor(blurredPart);
const auto contrast = Ui::CountContrast(
averageColor,
st::premiumButtonFg->c);
if (contrast < kMinAcceptableContrast) {
constexpr auto kDarkerBy = 0.2;
auto painterPart = QPainter(&blurredPart);
painterPart.setOpacity(kDarkerBy);
painterPart.fillRect(
QRect(QPoint(), partRect.size()),
Qt::black);
}
return Images::Circle(std::move(blurredPart));
}();
const auto partSkip = fullSize - partSize;
auto result = Images::Circle(BlurredDarkenedPart(
peer->generateUserpicImage(view, fullSize * ratio, 0),
QRect(
QPoint(partSkip, partSkip) * ratio,
QSize(partSize, partSize) * ratio)));
result.setDevicePixelRatio(ratio);
auto q = QPainter(&result);
PainterHighQualityEnabler hq(q);
@ -125,6 +105,28 @@ QRect CornerBadgeTTLRect(int photoSize) {
partSize);
}
QImage BlurredDarkenedPart(QImage image, QRect part) {
const auto ratio = style::DevicePixelRatio();
auto blurred = Images::BlurLargeImage(
std::move(image),
kBlurRadius).copy(part);
constexpr auto kMinAcceptableContrast = 4.5;
const auto averageColor = Ui::CountAverageColor(blurred);
const auto contrast = Ui::CountContrast(
averageColor,
st::premiumButtonFg->c);
if (contrast < kMinAcceptableContrast) {
constexpr auto kDarkerBy = 0.2;
auto painterPart = QPainter(&blurred);
painterPart.setOpacity(kDarkerBy);
painterPart.fillRect(QRect(QPoint(), part.size()), Qt::black);
}
blurred.setDevicePixelRatio(image.devicePixelRatio());
return blurred;
}
Row::CornerLayersManager::CornerLayersManager() = default;
bool Row::CornerLayersManager::isSameLayer(Layer layer) const {

View file

@ -39,6 +39,7 @@ class Entry;
enum class SortMode;
[[nodiscard]] QRect CornerBadgeTTLRect(int photoSize);
[[nodiscard]] QImage BlurredDarkenedPart(QImage image, QRect part);
class BasicRow {
public:

View file

@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/weak_ptr.h"
#include "ui/widgets/menu/menu_add_action_callback.h"
#include "ui/rp_widget.h"
#include "ui/effects/animations.h"
class QPainter;

View file

@ -1428,7 +1428,8 @@ void HistoryInner::onTouchScrollTimer() {
} else if (_touchScrollState == Ui::TouchScrollState::Auto || _touchScrollState == Ui::TouchScrollState::Acceleration) {
int32 elapsed = int32(nowTime - _touchTime);
QPoint delta = _touchSpeed * elapsed / 1000;
bool hasScrolled = _widget->touchScroll(delta);
bool hasScrolled = consumeScrollAction(delta)
|| _widget->touchScroll(delta);
if (_touchSpeed.isNull() || !hasScrolled) {
_touchScrollState = Ui::TouchScrollState::Manual;
@ -1625,7 +1626,9 @@ void HistoryInner::mouseActionUpdate(const QPoint &screenPos) {
void HistoryInner::touchScrollUpdated(const QPoint &screenPos) {
_touchPos = screenPos;
_widget->touchScroll(_touchPos - _touchPrevPos);
if (!consumeScrollAction(_touchPos - _touchPrevPos)) {
_widget->touchScroll(_touchPos - _touchPrevPos);
}
touchUpdateSpeed();
}
@ -3834,6 +3837,7 @@ void HistoryInner::mouseActionUpdate() {
}
Qt::CursorShape cur = style::cur_default;
_acceptsHorizontalScroll = dragState.horizontalScroll;
if (_mouseAction == MouseAction::None) {
_mouseCursorState = dragState.cursor;
if (dragState.link) {
@ -4447,6 +4451,17 @@ void HistoryInner::onParentGeometryChanged() {
}
}
bool HistoryInner::consumeScrollAction(QPoint delta) {
const auto horizontal = std::abs(delta.x()) > std::abs(delta.y());
if (!horizontal || !_acceptsHorizontalScroll || !Element::Moused()) {
return false;
}
const auto position = mapPointToItem(
mapFromGlobal(_mousePosition),
Element::Moused());
return Element::Moused()->consumeHorizontalScroll(position, delta.x());
}
Fn<HistoryView::ElementDelegate*()> HistoryInner::elementDelegateFactory(
FullMsgId itemId) const {
const auto weak = base::make_weak(_controller);

View file

@ -203,6 +203,7 @@ public:
bool tooltipWindowActive() const override;
void onParentGeometryChanged();
bool consumeScrollAction(QPoint delta);
[[nodiscard]] Fn<HistoryView::ElementDelegate*()> elementDelegateFactory(
FullMsgId itemId) const;
@ -490,6 +491,7 @@ private:
bool _recountedAfterPendingResizedItems = false;
bool _useCornerReaction = false;
bool _canHaveFromUserpicsSponsored = false;
bool _acceptsHorizontalScroll = false;
QPoint _trippleClickPoint;
base::Timer _trippleClickTimer;

View file

@ -317,6 +317,9 @@ public:
[[nodiscard]] bool isFakeBotAbout() const {
return _flags & MessageFlag::FakeBotAbout;
}
[[nodiscard]] bool showSimilarChannels() const {
return _flags & MessageFlag::ShowSimilarChannels;
}
[[nodiscard]] bool isRegular() const;
[[nodiscard]] bool isUploading() const;
void sendFailed();

View file

@ -557,7 +557,7 @@ not_null<HistoryItem*> GenerateJoinedMessage(
bool viaRequest) {
return history->makeMessage(
history->owner().nextLocalMessageId(),
MessageFlag::Local,
MessageFlag::Local | MessageFlag::ShowSimilarChannels,
inviteDate,
GenerateJoinedText(history, inviter, viaRequest));
}

View file

@ -135,6 +135,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/chat/chat_theme.h"
#include "ui/chat/chat_style.h"
#include "ui/chat/continuous_scroll.h"
#include "ui/widgets/elastic_scroll.h"
#include "ui/widgets/popup_menu.h"
#include "ui/item_text_options.h"
#include "main/main_session.h"
@ -271,6 +272,15 @@ HistoryWidget::HistoryWidget(
update();
}, lifetime());
base::install_event_filter(_scroll.data(), [=](not_null<QEvent*> e) {
const auto consumed = (e->type() == QEvent::Wheel)
&& _list
&& _list->consumeScrollAction(
Ui::ScrollDelta(static_cast<QWheelEvent*>(e.get())));
return consumed
? base::EventFilterResult::Cancel
: base::EventFilterResult::Continue;
});
_scroll->scrolls(
) | rpl::start_with_next([=] {
handleScroll();

View file

@ -24,8 +24,8 @@ TextState::TextState(
? CursorState::Text
: CursorState::None)
, link(state.link)
, afterSymbol(state.afterSymbol)
, symbol(state.symbol) {
, symbol(state.symbol)
, afterSymbol(state.afterSymbol) {
}
TextState::TextState(
@ -59,8 +59,8 @@ TextState::TextState(
? CursorState::Text
: CursorState::None)
, link(state.link)
, afterSymbol(state.afterSymbol)
, symbol(state.symbol) {
, symbol(state.symbol)
, afterSymbol(state.afterSymbol) {
}
TextState::TextState(std::nullptr_t, ClickHandlerPtr link)

View file

@ -50,10 +50,11 @@ struct TextState {
FullMsgId itemId;
CursorState cursor = CursorState::None;
ClickHandlerPtr link;
bool overMessageText = false;
bool afterSymbol = false;
bool customTooltip = false;
uint16 symbol = 0;
bool afterSymbol = false;
bool overMessageText = false;
bool customTooltip = false;
bool horizontalScroll = false;
QString customTooltipText;
};

View file

@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_message.h"
#include "history/view/media/history_view_media.h"
#include "history/view/media/history_view_media_grouped.h"
#include "history/view/media/history_view_similar_channels.h"
#include "history/view/media/history_view_sticker.h"
#include "history/view/media/history_view_large_emoji.h"
#include "history/view/media/history_view_custom_emoji.h"
@ -36,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_session_controller.h"
#include "ui/effects/path_shift_gradient.h"
#include "ui/effects/reaction_fly_animation.h"
#include "ui/effects/ripple_animation.h"
#include "ui/chat/chat_style.h"
#include "ui/toast/toast.h"
#include "ui/text/text_options.h"
@ -723,6 +725,8 @@ void Element::refreshMedia(Element *replacing) {
}
}
_media = media->createView(this, replacing);
} else if (item->showSimilarChannels()) {
_media = std::make_unique<SimilarChannels>(this);
} else if (isOnlyCustomEmoji()
&& Core::App().settings().largeEmoji()
&& !item->isSponsored()) {

View file

@ -515,6 +515,7 @@ public:
const Reactions::InlineList &reactions) const;
void clearCustomEmojiRepaint() const;
void hideSpoilers();
void repaint() const;
[[nodiscard]] ClickHandlerPtr fromPhotoLink() const {
return fromLink();
@ -531,6 +532,10 @@ public:
void overrideMedia(std::unique_ptr<Media> media);
virtual bool consumeHorizontalScroll(QPoint position, int delta) {
return false;
}
virtual ~Element();
static void Hovered(Element *view);
@ -546,8 +551,6 @@ public:
static void ClearGlobal();
protected:
void repaint() const;
void paintHighlight(
Painter &p,
const PaintContext &context,

View file

@ -52,10 +52,8 @@ void ValidateBackgroundEmoji(
}
const auto tag = Data::CustomEmojiSizeTag::Isolated;
if (!data->emoji) {
const auto repaint = crl::guard(view, [=] { view->repaint(); });
const auto owner = &view->history()->owner();
const auto repaint = crl::guard(view, [=] {
view->history()->owner().requestViewRepaint(view);
});
data->emoji = owner->customEmojiManager().create(
backgroundEmojiId,
repaint,
@ -779,7 +777,7 @@ void Reply::createRippleAnimation(
Ui::RippleAnimation::RoundRectMask(
size,
st::messageQuoteStyle.radius),
[=] { view->history()->owner().requestViewRepaint(view); });
[=] { view->repaint(); });
}
void Reply::saveRipplePoint(QPoint point) const {

View file

@ -411,6 +411,13 @@ QRect Service::innerGeometry() const {
return countGeometry();
}
bool Service::consumeHorizontalScroll(QPoint position, int delta) {
if (const auto media = this->media()) {
return media->consumeHorizontalScroll(position, delta);
}
return false;
}
QRect Service::countGeometry() const {
auto result = QRect(0, 0, width(), height());
if (delegate()->elementIsChatWide()) {
@ -429,7 +436,8 @@ QSize Service::performCountCurrentSize(int newWidth) {
return { newWidth, newHeight };
}
const auto media = this->media();
if (media && media->hideServiceText()) {
const auto mediaDisplayed = media && media->isDisplayed();
if (mediaDisplayed && media->hideServiceText()) {
newHeight += st::msgServiceMargin.top()
+ media->resizeGetHeight(newWidth)
+ st::msgServiceMargin.bottom();
@ -448,8 +456,10 @@ QSize Service::performCountCurrentSize(int newWidth) {
? minHeight()
: textHeightFor(nwidth);
newHeight += st::msgServicePadding.top() + st::msgServicePadding.bottom() + st::msgServiceMargin.top() + st::msgServiceMargin.bottom();
if (media) {
newHeight += st::msgServiceMargin.top() + media->resizeGetHeight(media->maxWidth());
if (mediaDisplayed) {
const auto mediaWidth = std::min(media->maxWidth(), nwidth);
newHeight += st::msgServiceMargin.top()
+ media->resizeGetHeight(mediaWidth);
}
}
@ -527,10 +537,11 @@ void Service::draw(Painter &p, const PaintContext &context) const {
p.setTextPalette(st->serviceTextPalette());
const auto media = this->media();
const auto onlyMedia = (media && media->hideServiceText());
const auto mediaDisplayed = media && media->isDisplayed();
const auto onlyMedia = (mediaDisplayed && media->hideServiceText());
if (!onlyMedia) {
if (media) {
if (mediaDisplayed) {
height -= margin.top() + media->height();
}
const auto trect = QRect(g.left(), margin.top(), g.width(), height)
@ -561,8 +572,8 @@ void Service::draw(Painter &p, const PaintContext &context) const {
.fullWidthSelection = false,
});
}
if (media) {
const auto left = margin.left() + (g.width() - media->maxWidth()) / 2;
if (mediaDisplayed) {
const auto left = margin.left() + (g.width() - media->width()) / 2;
const auto top = margin.top() + (onlyMedia ? 0 : (height + margin.top()));
p.translate(left, top);
media->draw(p, context.translated(-left, -top).withSelection({}));
@ -576,6 +587,7 @@ void Service::draw(Painter &p, const PaintContext &context) const {
PointState Service::pointState(QPoint point) const {
const auto media = this->media();
const auto mediaDisplayed = media && media->isDisplayed();
auto g = countGeometry();
if (g.width() < 1 || isHidden()) {
@ -588,7 +600,7 @@ PointState Service::pointState(QPoint point) const {
if (const auto bar = Get<UnreadBar>()) {
g.setTop(g.top() + bar->height());
}
if (media) {
if (mediaDisplayed) {
const auto centerPadding = (g.width() - media->width()) / 2;
const auto r = g - QMargins(centerPadding, 0, centerPadding, 0);
if (!r.contains(point)) {
@ -602,7 +614,8 @@ PointState Service::pointState(QPoint point) const {
TextState Service::textState(QPoint point, StateRequest request) const {
const auto item = data();
const auto media = this->media();
const auto onlyMedia = (media && media->hideServiceText());
const auto mediaDisplayed = media && media->isDisplayed();
const auto onlyMedia = (mediaDisplayed && media->hideServiceText());
auto result = TextState(item);
@ -622,8 +635,8 @@ TextState Service::textState(QPoint point, StateRequest request) const {
}
if (onlyMedia) {
return media->textState(point - QPoint(st::msgServiceMargin.left() + (g.width() - media->maxWidth()) / 2, st::msgServiceMargin.top()), request);
} else if (media) {
return media->textState(point - QPoint(st::msgServiceMargin.left() + (g.width() - media->width()) / 2, st::msgServiceMargin.top()), request);
} else if (mediaDisplayed) {
g.setHeight(g.height() - (st::msgServiceMargin.top() + media->height()));
}
auto trect = g.marginsAdded(-st::msgServicePadding);
@ -656,8 +669,8 @@ TextState Service::textState(QPoint point, StateRequest request) const {
result.link = same->lnk;
}
}
} else if (media) {
result = media->textState(point - QPoint(st::msgServiceMargin.left() + (g.width() - media->maxWidth()) / 2, st::msgServiceMargin.top() + g.height() + st::msgServiceMargin.top()), request);
} else if (mediaDisplayed) {
result = media->textState(point - QPoint(st::msgServiceMargin.left() + (g.width() - media->width()) / 2, st::msgServiceMargin.top() + g.height() + st::msgServiceMargin.top()), request);
}
return result;
}

View file

@ -52,6 +52,8 @@ public:
QRect innerGeometry() const override;
bool consumeHorizontalScroll(QPoint position, int delta) override;
private:
[[nodiscard]] QRect countGeometry() const;

View file

@ -413,13 +413,12 @@ void Game::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {
if (!_ripple) {
const auto full = QRect(0, 0, width(), height());
const auto outer = full.marginsRemoved(inBubblePadding());
const auto owner = &parent()->history()->owner();
_ripple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
Ui::RippleAnimation::RoundRectMask(
outer.size(),
_st.radius),
[=] { owner->requestViewRepaint(parent()); });
[=] { repaint(); });
}
_ripple->add(_lastPoint);
} else if (_ripple) {

View file

@ -14,7 +14,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_channel.h"
#include "data/data_document.h"
#include "data/data_media_types.h"
#include "data/data_session.h"
#include "dialogs/ui/dialogs_stories_content.h"
#include "dialogs/ui/dialogs_stories_list.h"
#include "history/history.h"
@ -348,9 +347,7 @@ void Giveaway::paintChannels(
const auto &thumbnail = channel.thumbnail;
const auto &geometry = channel.geometry;
if (!_subscribedToThumbnails) {
thumbnail->subscribeToUpdates([view = parent()] {
view->history()->owner().requestViewRepaint(view);
});
thumbnail->subscribeToUpdates([=] { repaint(); });
}
const auto colorIndex = channel.colorIndex;
@ -487,13 +484,12 @@ void Giveaway::clickHandlerPressedChanged(
}
if (pressed) {
if (!channel.ripple) {
const auto owner = &parent()->history()->owner();
channel.ripple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
Ui::RippleAnimation::RoundRectMask(
channel.geometry.size(),
channel.geometry.height() / 2),
[=] { owner->requestViewRepaint(parent()); });
[=] { repaint(); });
}
channel.ripple->add(_lastPoint - channel.geometry.topLeft());
} else if (channel.ripple) {

View file

@ -198,10 +198,6 @@ SelectedQuote Media::selectedQuote(TextSelection selection) const {
return {};
}
bool Media::isDisplayed() const {
return true;
}
QSize Media::countCurrentSize(int newWidth) {
return QSize(qMin(newWidth, maxWidth()), minHeight());
}
@ -285,7 +281,7 @@ void Media::fillImageSpoiler(
void Media::createSpoilerLink(not_null<MediaSpoiler*> spoiler) {
const auto weak = base::make_weak(this);
spoiler->link = std::make_shared<LambdaClickHandler>([=](
spoiler->link = std::make_shared<LambdaClickHandler>([weak, spoiler](
const ClickContext &context) {
const auto button = context.button;
const auto media = weak.get();
@ -295,15 +291,15 @@ void Media::createSpoilerLink(not_null<MediaSpoiler*> spoiler) {
const auto view = media->parent();
spoiler->revealed = true;
spoiler->revealAnimation.start([=] {
media->history()->owner().requestViewRepaint(view);
view->repaint();
}, 0., 1., st::fadeWrapDuration);
media->history()->owner().requestViewRepaint(view);
view->repaint();
media->history()->owner().registerShownSpoiler(view);
});
}
void Media::repaint() const {
history()->owner().requestViewRepaint(_parent);
_parent->repaint();
}
Ui::Text::String Media::createCaption(not_null<HistoryItem*> item) const {

View file

@ -96,7 +96,9 @@ public:
return {};
}
[[nodiscard]] virtual bool isDisplayed() const;
[[nodiscard]] virtual bool isDisplayed() const {
return true;
}
virtual void updateNeedBubbleState() {
}
[[nodiscard]] virtual bool hasTextForCopy() const {
@ -335,6 +337,10 @@ public:
virtual void parentTextUpdated() {
}
virtual bool consumeHorizontalScroll(QPoint position, int delta) {
return false;
}
virtual ~Media() = default;
protected:

View file

@ -0,0 +1,496 @@
/*
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 "history/view/media/history_view_similar_channels.h"
#include "api/api_chat_participants.h"
#include "apiwrap.h"
#include "boxes/peer_lists_box.h"
#include "core/click_handler_types.h"
#include "data/data_channel.h"
#include "data/data_session.h"
#include "dialogs/ui/dialogs_stories_content.h"
#include "dialogs/ui/dialogs_stories_list.h"
#include "history/view/history_view_element.h"
#include "history/view/history_view_cursor_state.h"
#include "history/history.h"
#include "history/history_item.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/effects/ripple_animation.h"
#include "ui/painter.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
namespace HistoryView {
namespace {
class SimilarChannelsController final : public PeerListController {
public:
SimilarChannelsController(
not_null<Window::SessionController*> controller,
std::vector<not_null<ChannelData*>> channels);
void prepare() override;
void loadMoreRows() override;
void rowClicked(not_null<PeerListRow*> row) override;
Main::Session &session() const override;
private:
const not_null<Window::SessionController*> _controller;
const std::vector<not_null<ChannelData*>> _channels;
};
SimilarChannelsController::SimilarChannelsController(
not_null<Window::SessionController*> controller,
std::vector<not_null<ChannelData*>> channels)
: _controller(controller)
, _channels(std::move(channels)) {
}
void SimilarChannelsController::prepare() {
for (const auto &channel : _channels) {
auto row = std::make_unique<PeerListRow>(channel);
if (const auto count = channel->membersCount(); count > 1) {
row->setCustomStatus(tr::lng_chat_status_subscribers(
tr::now,
lt_count,
count));
}
delegate()->peerListAppendRow(std::move(row));
}
delegate()->peerListRefreshRows();
}
void SimilarChannelsController::loadMoreRows() {
}
void SimilarChannelsController::rowClicked(not_null<PeerListRow*> row) {
const auto other = ClickHandlerContext{
.sessionWindow = _controller,
.show = _controller->uiShow(),
};
row->peer()->openLink()->onClick({
Qt::LeftButton,
QVariant::fromValue(other)
});
}
Main::Session &SimilarChannelsController::session() const {
return _channels.front()->session();
}
[[nodiscard]] object_ptr<Ui::BoxContent> SimilarChannelsBox(
not_null<Window::SessionController*> controller,
const std::vector<not_null<ChannelData*>> &channels) {
const auto initBox = [=](not_null<PeerListBox*> box) {
box->setTitle(tr::lng_similar_channels_title());
box->addButton(tr::lng_close(), [=] { box->closeBox(); });
};
return Box<PeerListBox>(
std::make_unique<SimilarChannelsController>(controller, channels),
initBox);
}
} // namespace
SimilarChannels::SimilarChannels(not_null<Element*> parent)
: Media(parent) {
}
SimilarChannels::~SimilarChannels() {
if (hasHeavyPart()) {
unloadHeavyPart();
parent()->checkHeavyPart();
}
}
void SimilarChannels::clickHandlerActiveChanged(
const ClickHandlerPtr &p,
bool active) {
}
void SimilarChannels::clickHandlerPressedChanged(
const ClickHandlerPtr &p,
bool pressed) {
for (auto &channel : _channels) {
if (channel.link != p) {
continue;
}
if (pressed) {
if (!channel.ripple) {
channel.ripple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
Ui::RippleAnimation::RoundRectMask(
channel.geometry.size(),
st::roundRadiusLarge),
[=] { repaint(); });
}
channel.ripple->add(_lastPoint);
} else if (channel.ripple) {
channel.ripple->lastStop();
}
break;
}
}
void SimilarChannels::draw(Painter &p, const PaintContext &context) const {
const auto large = Ui::BubbleCornerRounding::Large;
const auto geometry = QRect(0, 0, width(), height());
Ui::PaintBubble(
p,
Ui::SimpleBubble{
.st = context.st,
.geometry = geometry,
.pattern = context.bubblesPattern,
.patternViewport = context.viewport,
.outerWidth = width(),
.rounding = { large, large, large, large },
});
const auto stm = context.messageStyle();
{
auto hq = PainterHighQualityEnabler(p);
auto path = QPainterPath();
const auto x = geometry.center().x();
const auto y = geometry.y();
const auto size = st::chatSimilarArrowSize;
path.moveTo(x, y - size);
path.lineTo(x + size, y);
path.lineTo(x - size, y);
path.lineTo(x, y - size);
p.fillPath(path, stm->msgBg);
}
const auto photo = st::chatSimilarChannelPhoto;
const auto padding = st::chatSimilarChannelPadding;
p.setClipRect(geometry);
_hasHeavyPart = 1;
const auto drawOne = [&](const Channel &channel) {
const auto geometry = channel.geometry.translated(-_scrollLeft, 0);
const auto right = geometry.x() + geometry.width();
if (right <= 0) {
return;
}
if (!channel.subscribed) {
channel.subscribed = true;
const auto raw = channel.thumbnail.get();
const auto view = parent();
channel.thumbnail->subscribeToUpdates([=] {
for (const auto &channel : _channels) {
if (channel.thumbnail.get() == raw) {
channel.participantsBgValid = false;
repaint();
}
}
});
}
auto cachedp = std::optional<Painter>();
const auto cached = (geometry.x() < padding.left())
|| (right > width() - padding.right());
if (cached) {
ensureCacheReady(geometry.size());
_roundedCache.fill(Qt::transparent);
cachedp.emplace(&_roundedCache);
cachedp->translate(-geometry.topLeft());
}
const auto q = cachedp ? &*cachedp : &p;
if (channel.ripple) {
q->setOpacity(st::historyPollRippleOpacity);
channel.ripple->paint(
*q,
geometry.x(),
geometry.y(),
width(),
&stm->msgWaveformInactive->c);
if (channel.ripple->empty()) {
channel.ripple.reset();
}
q->setOpacity(1.);
}
q->drawImage(
geometry.x() + padding.left(),
geometry.y() + padding.top(),
channel.thumbnail->image(st::chatSimilarChannelPhoto));
if (!channel.participants.isEmpty()) {
validateParticipansBg(channel);
const auto participants = channel.participantsRect.translated(
QPoint(-_scrollLeft, 0));
q->drawImage(participants.topLeft(), channel.participantsBg);
const auto badge = participants.marginsRemoved(
st::chatSimilarBadgePadding);
const auto &icon = st::chatSimilarBadgeIcon;
const auto &font = st::chatSimilarBadgeFont;
const auto position = st::chatSimilarBadgeIconPosition;
const auto ascent = font->ascent;
icon.paint(*q, badge.topLeft() + position, width());
q->setFont(font);
q->setPen(st::premiumButtonFg);
q->drawText(
badge.x() + position.x() + icon.width(),
badge.y() + font->ascent,
channel.participants);
}
q->setPen(stm->historyTextFg);
channel.name.drawLeftElided(
*q,
geometry.x() + st::normalFont->spacew,
geometry.y() + st::chatSimilarNameTop,
(geometry.width() - 2 * st::normalFont->spacew),
width(),
2,
style::al_top);
if (cachedp) {
q->setCompositionMode(QPainter::CompositionMode_DestinationIn);
const auto corners = _roundedCorners.data();
const auto side = st::bubbleRadiusLarge;
q->drawImage(0, 0, corners[Images::kTopLeft]);
q->drawImage(width() - side, 0, corners[Images::kTopRight]);
q->drawImage(0, height() - side, corners[Images::kBottomLeft]);
q->drawImage(
QPoint(width() - side, height() - side),
corners[Images::kBottomRight]);
cachedp.reset();
p.drawImage(geometry.topLeft(), _roundedCache);
}
};
for (const auto &channel : _channels) {
if (channel.geometry.x() >= _scrollLeft + width()) {
break;
}
drawOne(channel);
}
p.setFont(st::chatSimilarTitle);
p.drawTextLeft(
st::chatSimilarTitlePosition.x(),
st::chatSimilarTitlePosition.y(),
width(),
_title);
if (!_hasViewAll) {
return;
}
p.setFont(ClickHandler::showAsActive(_viewAllLink)
? st::normalFont->underline()
: st::normalFont);
p.setPen(stm->textPalette.linkFg);
const auto add = st::normalFont->ascent - st::chatSimilarTitle->ascent;
p.drawTextRight(
st::chatSimilarTitlePosition.x(),
st::chatSimilarTitlePosition.y() + add,
width(),
_viewAll);
p.setClipping(false);
}
void SimilarChannels::validateParticipansBg(const Channel &channel) const {
if (channel.participantsBgValid) {
return;
}
channel.participantsBgValid = true;
const auto photo = st::chatSimilarChannelPhoto;
const auto width = channel.participantsRect.width();
const auto height = channel.participantsRect.height();
const auto ratio = style::DevicePixelRatio();
auto result = QImage(
channel.participantsRect.size() * ratio,
QImage::Format_ARGB32_Premultiplied);
auto color = Ui::CountAverageColor(
channel.thumbnail->image(photo).copy(
QRect(photo / 3, photo / 3, photo / 3, photo / 3)));
const auto lightness = color.lightness();
if (!base::in_range(lightness, 160, 208)) {
color = color.toHsl();
color.setHsl(
color.hue(),
color.saturation(),
std::clamp(lightness, 160, 208));
color = color.toRgb();
}
result.fill(color);
result.setDevicePixelRatio(ratio);
const auto radius = height / 2;
auto corners = Images::CornersMask(radius);
auto p = QPainter(&result);
p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
p.drawImage(0, 0, corners[Images::kTopLeft]);
p.drawImage(width - radius, 0, corners[Images::kTopRight]);
p.drawImage(0, height - radius, corners[Images::kBottomLeft]);
p.drawImage(
width - radius,
height - radius,
corners[Images::kBottomRight]);
p.end();
channel.participantsBg = std::move(result);
}
void SimilarChannels::ensureCacheReady(QSize size) const {
const auto ratio = style::DevicePixelRatio();
if (_roundedCache.size() != size * ratio) {
_roundedCache = QImage(
size * ratio,
QImage::Format_ARGB32_Premultiplied);
_roundedCache.setDevicePixelRatio(ratio);
}
const auto radius = st::bubbleRadiusLarge;
if (_roundedCorners.front().size() != QSize(radius, radius) * ratio) {
_roundedCorners = Images::CornersMask(radius);
}
}
TextState SimilarChannels::textState(
QPoint point,
StateRequest request) const {
auto result = TextState();
result.horizontalScroll = (_scrollMax > 0);
const auto skip = st::chatSimilarTitlePosition;
const auto viewWidth = _hasViewAll ? (_viewAllWidth + 2 * skip.x()) : 0;
const auto viewHeight = st::normalFont->height + 2 * skip.y();
const auto viewLeft = width() - viewWidth;
if (QRect(viewLeft, 0, viewWidth, viewHeight).contains(point)) {
if (!_viewAllLink) {
const auto channel = parent()->history()->peer->asChannel();
Assert(channel != nullptr);
_viewAllLink = std::make_shared<LambdaClickHandler>([=](
ClickContext context) {
Assert(channel != nullptr);
const auto api = &channel->session().api();
const auto &list = api->chatParticipants().similar(channel);
if (list.empty()) {
return;
}
const auto my = context.other.value<ClickHandlerContext>();
if (const auto strong = my.sessionWindow.get()) {
strong->show(SimilarChannelsBox(strong, list));
}
});
}
result.link = _viewAllLink;
return result;
}
for (const auto &channel : _channels) {
if (channel.geometry.translated(-_scrollLeft, 0).contains(point)) {
result.link = channel.link;
_lastPoint = point
+ QPoint(_scrollLeft, 0)
- channel.geometry.topLeft();
break;
}
}
return result;
}
QSize SimilarChannels::countOptimalSize() {
const auto channel = parent()->history()->peer->asChannel();
Assert(channel != nullptr);
_channels.clear();
const auto api = &channel->session().api();
const auto similar = api->chatParticipants().similar(channel);
if (similar.empty()) {
return {};
}
_channels.reserve(similar.size());
auto x = st::chatSimilarPadding.left();
auto y = st::chatSimilarPadding.top();
const auto skip = st::chatSimilarSkip;
const auto photo = st::chatSimilarChannelPhoto;
const auto inner = QRect(0, 0, photo, photo);
const auto outer = inner.marginsAdded(st::chatSimilarChannelPadding);
for (const auto &channel : similar) {
const auto participants = channel->membersCount();
const auto count = (participants > 1)
? Lang::FormatCountToShort(participants).string
: QString();
_channels.push_back({
.geometry = QRect(QPoint(x, y), outer.size()),
.name = Ui::Text::String(
st::chatSimilarName,
channel->name(),
kDefaultTextOptions,
st::chatSimilarChannelPhoto),
.thumbnail = Dialogs::Stories::MakeUserpicThumbnail(channel),
.link = channel->openLink(),
.participants = count,
});
if (!count.isEmpty()) {
const auto length = st::chatSimilarBadgeFont->width(count);
const auto width = length + st::chatSimilarBadgeIcon.width();
const auto delta = (outer.width() - width) / 2;
const auto badge = QRect(
x + delta,
y + st::chatSimilarBadgeTop,
outer.width() - 2 * delta,
st::chatSimilarBadgeFont->height);
_channels.back().participantsRect = badge.marginsAdded(
st::chatSimilarBadgePadding);
}
x += outer.width() + skip;
}
_title = tr::lng_similar_channels_title(tr::now);
_titleWidth = st::chatSimilarTitle->width(_title);
_viewAll = tr::lng_similar_channels_view_all(tr::now);
_viewAllWidth = st::normalFont->width(_viewAll);
const auto count = int(_channels.size());
const auto desired = (count ? (x - skip) : x)
- st::chatSimilarPadding.left();
const auto full = QRect(0, 0, desired, outer.height());
const auto bubble = full.marginsAdded(st::chatSimilarPadding);
_fullWidth = bubble.width();
const auto titleSkip = st::chatSimilarTitlePosition.x();
const auto min = _titleWidth + 2 * titleSkip;
const auto limited = std::max(
std::min(_fullWidth, st::chatSimilarWidthMax),
min);
if (limited > _fullWidth) {
const auto shift = (limited - _fullWidth) / 2;
for (auto &channel : _channels) {
channel.geometry.translate(shift, 0);
}
}
return { limited, bubble.height() };
}
QSize SimilarChannels::countCurrentSize(int newWidth) {
_scrollMax = std::max(_fullWidth - newWidth, 0);
_scrollLeft = std::clamp(_scrollLeft, uint32(), _scrollMax);
_hasViewAll = (_scrollMax != 0) ? 1 : 0;
return { newWidth, minHeight() };
}
bool SimilarChannels::hasHeavyPart() const {
return _hasHeavyPart != 0;
}
void SimilarChannels::unloadHeavyPart() {
_hasHeavyPart = 0;
for (const auto &channel : _channels) {
channel.subscribed = false;
channel.thumbnail->subscribeToUpdates(nullptr);
}
}
bool SimilarChannels::consumeHorizontalScroll(QPoint position, int delta) {
if (_scrollMax == 0) {
return false;
}
const auto left = _scrollLeft;
_scrollLeft = std::clamp(
int(_scrollLeft) - delta,
0,
int(_scrollMax));
if (_scrollLeft == left) {
return false;
}
repaint();
return true;
}
} // namespace HistoryView

View file

@ -0,0 +1,98 @@
/*
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
#include "history/view/media/history_view_media.h"
namespace Dialogs::Stories {
class Thumbnail;
} // namespace Dialogs::Stories
namespace Ui {
class RippleAnimation;
} // namespace Ui
namespace HistoryView {
class SimilarChannels final : public Media {
public:
explicit SimilarChannels(not_null<Element*> parent);
~SimilarChannels();
void draw(Painter &p, const PaintContext &context) const override;
TextState textState(QPoint point, StateRequest request) const override;
void clickHandlerActiveChanged(
const ClickHandlerPtr &p,
bool active) override;
void clickHandlerPressedChanged(
const ClickHandlerPtr &p,
bool pressed) override;
bool toggleSelectionByHandlerClick(
const ClickHandlerPtr &p) const override {
return false;
}
bool dragItemByHandler(const ClickHandlerPtr &p) const override {
return false;
}
bool needsBubble() const override {
return false;
}
bool customInfoLayout() const override {
return true;
}
bool isDisplayed() const override {
return !_channels.empty();
}
void unloadHeavyPart() override;
bool hasHeavyPart() const override;
bool consumeHorizontalScroll(QPoint position, int delta) override;
private:
using Thumbnail = Dialogs::Stories::Thumbnail;
struct Channel {
QRect geometry;
Ui::Text::String name;
std::shared_ptr<Thumbnail> thumbnail;
ClickHandlerPtr link;
QString participants;
QRect participantsRect;
mutable QImage participantsBg;
mutable std::unique_ptr<Ui::RippleAnimation> ripple;
mutable bool subscribed = false;
mutable bool participantsBgValid = false;
};
void ensureCacheReady(QSize size) const;
void validateParticipansBg(const Channel &channel) const;
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;
QString _title, _viewAll;
mutable QImage _roundedCache;
mutable std::array<QImage, 4> _roundedCorners;
mutable QPoint _lastPoint;
int _titleWidth = 0;
int _viewAllWidth = 0;
int _fullWidth = 0;
uint32 _scrollLeft : 15 = 0;
uint32 _scrollMax : 15 = 0;
uint32 _hasViewAll : 1 = 0;
mutable uint32 _hasHeavyPart : 1 = 0;
std::vector<Channel> _channels;
mutable ClickHandlerPtr _viewAllLink;
};
} // namespace HistoryView

View file

@ -977,3 +977,23 @@ chatGiveawayCountriesSkip: 16px;
chatGiveawayDateTop: 6px;
chatGiveawayDateSkip: 4px;
chatGiveawayBottomSkip: 16px;
chatSimilarRadius: 12px;
chatSimilarArrowSize: 6px;
chatSimilarTitle: semiboldFont;
chatSimilarTitlePosition: point(15px, 9px);
chatSimilarPadding: margins(8px, 32px, 8px, 4px);
chatSimilarChannelPadding: margins(8px, 5px, 8px, 37px);
chatSimilarChannelPhoto: 50px;
chatSimilarBadgePadding: margins(2px, 0px, 3px, 1px);
chatSimilarBadgeTop: 43px;
chatSimilarBadgeIcon: icon{{ "chat/mini_subscribers", premiumButtonFg }};
chatSimilarBadgeIconPosition: point(0px, 1px);
chatSimilarBadgeFont: font(10px bold);
chatSimilarNameTop: 59px;
chatSimilarName: TextStyle(defaultTextStyle) {
font: font(12px);
lineHeight: 14px;
}
chatSimilarWidthMax: 424px;
chatSimilarSkip: 12px;