mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-06-05 06:33:57 +02:00
Show similar channels under join message.
This commit is contained in:
parent
91fba41e2c
commit
36a8c49213
30 changed files with 814 additions and 74 deletions
|
@ -713,6 +713,8 @@ PRIVATE
|
||||||
history/view/media/history_view_premium_gift.h
|
history/view/media/history_view_premium_gift.h
|
||||||
history/view/media/history_view_service_box.cpp
|
history/view/media/history_view_service_box.cpp
|
||||||
history/view/media/history_view_service_box.h
|
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.cpp
|
||||||
history/view/media/history_view_slot_machine.h
|
history/view/media/history_view_slot_machine.h
|
||||||
history/view/media/history_view_sticker.cpp
|
history/view/media/history_view_sticker.cpp
|
||||||
|
|
BIN
Telegram/Resources/icons/chat/mini_subscribers.png
Normal file
BIN
Telegram/Resources/icons/chat/mini_subscribers.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 261 B |
BIN
Telegram/Resources/icons/chat/mini_subscribers@2x.png
Normal file
BIN
Telegram/Resources/icons/chat/mini_subscribers@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 399 B |
BIN
Telegram/Resources/icons/chat/mini_subscribers@3x.png
Normal file
BIN
Telegram/Resources/icons/chat/mini_subscribers@3x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 557 B |
|
@ -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_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_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#one" = "for {count} month";
|
||||||
"lng_premium_gift_duration_months#other" = "for {count} months";
|
"lng_premium_gift_duration_months#other" = "for {count} months";
|
||||||
"lng_premium_gift_duration_years#one" = "for {count} year";
|
"lng_premium_gift_duration_years#one" = "for {count} year";
|
||||||
|
|
|
@ -211,6 +211,23 @@ void ApplyBotsList(
|
||||||
Data::PeerUpdate::Flag::FullInfo);
|
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
|
} // namespace
|
||||||
|
|
||||||
ChatParticipant::ChatParticipant(
|
ChatParticipant::ChatParticipant(
|
||||||
|
@ -559,6 +576,7 @@ void ChatParticipants::requestSelf(not_null<ChannelData*> channel) {
|
||||||
UserId inviter = -1,
|
UserId inviter = -1,
|
||||||
TimeId inviteDate = 0,
|
TimeId inviteDate = 0,
|
||||||
bool inviteViaRequest = false) {
|
bool inviteViaRequest = false) {
|
||||||
|
const auto dateChanged = (channel->inviteDate != inviteDate);
|
||||||
channel->inviter = inviter;
|
channel->inviter = inviter;
|
||||||
channel->inviteDate = inviteDate;
|
channel->inviteDate = inviteDate;
|
||||||
channel->inviteViaRequest = inviteViaRequest;
|
channel->inviteViaRequest = inviteViaRequest;
|
||||||
|
@ -569,6 +587,9 @@ void ChatParticipants::requestSelf(not_null<ChannelData*> channel) {
|
||||||
} else {
|
} else {
|
||||||
history->owner().histories().requestDialogEntry(history);
|
history->owner().histories().requestDialogEntry(history);
|
||||||
}
|
}
|
||||||
|
if (dateChanged) {
|
||||||
|
loadSimilarChannels(channel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
_selfParticipantRequests.emplace(channel);
|
_selfParticipantRequests.emplace(channel);
|
||||||
|
@ -685,4 +706,35 @@ void ChatParticipants::unblock(
|
||||||
_kickRequests.emplace(kick, requestId);
|
_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
|
} // namespace Api
|
||||||
|
|
|
@ -120,7 +120,19 @@ public:
|
||||||
not_null<ChannelData*> channel,
|
not_null<ChannelData*> channel,
|
||||||
not_null<PeerData*> participant);
|
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:
|
private:
|
||||||
|
struct SimilarChannels {
|
||||||
|
std::vector<not_null<ChannelData*>> list;
|
||||||
|
mtpRequestId requestId = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
void loadSimilarChannels(not_null<ChannelData*> channel);
|
||||||
|
|
||||||
MTP::Sender _api;
|
MTP::Sender _api;
|
||||||
|
|
||||||
using PeerRequests = base::flat_map<PeerData*, mtpRequestId>;
|
using PeerRequests = base::flat_map<PeerData*, mtpRequestId>;
|
||||||
|
@ -143,6 +155,9 @@ private:
|
||||||
not_null<PeerData*>>;
|
not_null<PeerData*>>;
|
||||||
base::flat_map<KickRequest, mtpRequestId> _kickRequests;
|
base::flat_map<KickRequest, mtpRequestId> _kickRequests;
|
||||||
|
|
||||||
|
base::flat_map<not_null<ChannelData*>, SimilarChannels> _similar;
|
||||||
|
rpl::event_stream<not_null<ChannelData*>> _similarLoaded;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Api
|
} // namespace Api
|
||||||
|
|
|
@ -309,6 +309,8 @@ enum class MessageFlag : uint64 {
|
||||||
|
|
||||||
// If not set then we need to refresh _displayFrom value.
|
// If not set then we need to refresh _displayFrom value.
|
||||||
DisplayFromChecked = (1ULL << 40),
|
DisplayFromChecked = (1ULL << 40),
|
||||||
|
|
||||||
|
ShowSimilarChannels = (1ULL << 41),
|
||||||
};
|
};
|
||||||
inline constexpr bool is_flag_type(MessageFlag) { return true; }
|
inline constexpr bool is_flag_type(MessageFlag) { return true; }
|
||||||
using MessageFlags = base::flags<MessageFlag>;
|
using MessageFlags = base::flags<MessageFlag>;
|
||||||
|
|
|
@ -37,6 +37,7 @@ namespace {
|
||||||
constexpr auto kTopLayer = 2;
|
constexpr auto kTopLayer = 2;
|
||||||
constexpr auto kBottomLayer = 1;
|
constexpr auto kBottomLayer = 1;
|
||||||
constexpr auto kNoneLayer = 0;
|
constexpr auto kNoneLayer = 0;
|
||||||
|
constexpr auto kBlurRadius = 24;
|
||||||
|
|
||||||
[[nodiscard]] QImage CornerBadgeTTL(
|
[[nodiscard]] QImage CornerBadgeTTL(
|
||||||
not_null<PeerData*> peer,
|
not_null<PeerData*> peer,
|
||||||
|
@ -46,38 +47,17 @@ constexpr auto kNoneLayer = 0;
|
||||||
if (!ttl) {
|
if (!ttl) {
|
||||||
return QImage();
|
return QImage();
|
||||||
}
|
}
|
||||||
constexpr auto kBlurRadius = 24;
|
|
||||||
|
|
||||||
const auto ratio = style::DevicePixelRatio();
|
const auto ratio = style::DevicePixelRatio();
|
||||||
const auto fullSize = photoSize;
|
const auto fullSize = photoSize;
|
||||||
const auto blurredFull = Images::BlurLargeImage(
|
|
||||||
peer->generateUserpicImage(view, fullSize * ratio, 0),
|
|
||||||
kBlurRadius);
|
|
||||||
const auto partRect = CornerBadgeTTLRect(fullSize);
|
const auto partRect = CornerBadgeTTLRect(fullSize);
|
||||||
const auto &partSize = partRect.width();
|
const auto &partSize = partRect.width();
|
||||||
auto result = [&] {
|
const auto partSkip = fullSize - partSize;
|
||||||
auto blurredPart = blurredFull.copy(
|
auto result = Images::Circle(BlurredDarkenedPart(
|
||||||
blurredFull.width() - partSize * ratio,
|
peer->generateUserpicImage(view, fullSize * ratio, 0),
|
||||||
blurredFull.height() - partSize * ratio,
|
QRect(
|
||||||
partSize * ratio,
|
QPoint(partSkip, partSkip) * ratio,
|
||||||
partSize * ratio);
|
QSize(partSize, partSize) * ratio)));
|
||||||
blurredPart.setDevicePixelRatio(ratio);
|
result.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));
|
|
||||||
}();
|
|
||||||
|
|
||||||
auto q = QPainter(&result);
|
auto q = QPainter(&result);
|
||||||
PainterHighQualityEnabler hq(q);
|
PainterHighQualityEnabler hq(q);
|
||||||
|
@ -125,6 +105,28 @@ QRect CornerBadgeTTLRect(int photoSize) {
|
||||||
partSize);
|
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;
|
Row::CornerLayersManager::CornerLayersManager() = default;
|
||||||
|
|
||||||
bool Row::CornerLayersManager::isSameLayer(Layer layer) const {
|
bool Row::CornerLayersManager::isSameLayer(Layer layer) const {
|
||||||
|
|
|
@ -39,6 +39,7 @@ class Entry;
|
||||||
enum class SortMode;
|
enum class SortMode;
|
||||||
|
|
||||||
[[nodiscard]] QRect CornerBadgeTTLRect(int photoSize);
|
[[nodiscard]] QRect CornerBadgeTTLRect(int photoSize);
|
||||||
|
[[nodiscard]] QImage BlurredDarkenedPart(QImage image, QRect part);
|
||||||
|
|
||||||
class BasicRow {
|
class BasicRow {
|
||||||
public:
|
public:
|
||||||
|
|
|
@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "base/weak_ptr.h"
|
#include "base/weak_ptr.h"
|
||||||
#include "ui/widgets/menu/menu_add_action_callback.h"
|
#include "ui/widgets/menu/menu_add_action_callback.h"
|
||||||
#include "ui/rp_widget.h"
|
#include "ui/rp_widget.h"
|
||||||
|
#include "ui/effects/animations.h"
|
||||||
|
|
||||||
class QPainter;
|
class QPainter;
|
||||||
|
|
||||||
|
|
|
@ -1428,7 +1428,8 @@ void HistoryInner::onTouchScrollTimer() {
|
||||||
} else if (_touchScrollState == Ui::TouchScrollState::Auto || _touchScrollState == Ui::TouchScrollState::Acceleration) {
|
} else if (_touchScrollState == Ui::TouchScrollState::Auto || _touchScrollState == Ui::TouchScrollState::Acceleration) {
|
||||||
int32 elapsed = int32(nowTime - _touchTime);
|
int32 elapsed = int32(nowTime - _touchTime);
|
||||||
QPoint delta = _touchSpeed * elapsed / 1000;
|
QPoint delta = _touchSpeed * elapsed / 1000;
|
||||||
bool hasScrolled = _widget->touchScroll(delta);
|
bool hasScrolled = consumeScrollAction(delta)
|
||||||
|
|| _widget->touchScroll(delta);
|
||||||
|
|
||||||
if (_touchSpeed.isNull() || !hasScrolled) {
|
if (_touchSpeed.isNull() || !hasScrolled) {
|
||||||
_touchScrollState = Ui::TouchScrollState::Manual;
|
_touchScrollState = Ui::TouchScrollState::Manual;
|
||||||
|
@ -1625,7 +1626,9 @@ void HistoryInner::mouseActionUpdate(const QPoint &screenPos) {
|
||||||
|
|
||||||
void HistoryInner::touchScrollUpdated(const QPoint &screenPos) {
|
void HistoryInner::touchScrollUpdated(const QPoint &screenPos) {
|
||||||
_touchPos = screenPos;
|
_touchPos = screenPos;
|
||||||
_widget->touchScroll(_touchPos - _touchPrevPos);
|
if (!consumeScrollAction(_touchPos - _touchPrevPos)) {
|
||||||
|
_widget->touchScroll(_touchPos - _touchPrevPos);
|
||||||
|
}
|
||||||
touchUpdateSpeed();
|
touchUpdateSpeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3834,6 +3837,7 @@ void HistoryInner::mouseActionUpdate() {
|
||||||
}
|
}
|
||||||
|
|
||||||
Qt::CursorShape cur = style::cur_default;
|
Qt::CursorShape cur = style::cur_default;
|
||||||
|
_acceptsHorizontalScroll = dragState.horizontalScroll;
|
||||||
if (_mouseAction == MouseAction::None) {
|
if (_mouseAction == MouseAction::None) {
|
||||||
_mouseCursorState = dragState.cursor;
|
_mouseCursorState = dragState.cursor;
|
||||||
if (dragState.link) {
|
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(
|
Fn<HistoryView::ElementDelegate*()> HistoryInner::elementDelegateFactory(
|
||||||
FullMsgId itemId) const {
|
FullMsgId itemId) const {
|
||||||
const auto weak = base::make_weak(_controller);
|
const auto weak = base::make_weak(_controller);
|
||||||
|
|
|
@ -203,6 +203,7 @@ public:
|
||||||
bool tooltipWindowActive() const override;
|
bool tooltipWindowActive() const override;
|
||||||
|
|
||||||
void onParentGeometryChanged();
|
void onParentGeometryChanged();
|
||||||
|
bool consumeScrollAction(QPoint delta);
|
||||||
|
|
||||||
[[nodiscard]] Fn<HistoryView::ElementDelegate*()> elementDelegateFactory(
|
[[nodiscard]] Fn<HistoryView::ElementDelegate*()> elementDelegateFactory(
|
||||||
FullMsgId itemId) const;
|
FullMsgId itemId) const;
|
||||||
|
@ -490,6 +491,7 @@ private:
|
||||||
bool _recountedAfterPendingResizedItems = false;
|
bool _recountedAfterPendingResizedItems = false;
|
||||||
bool _useCornerReaction = false;
|
bool _useCornerReaction = false;
|
||||||
bool _canHaveFromUserpicsSponsored = false;
|
bool _canHaveFromUserpicsSponsored = false;
|
||||||
|
bool _acceptsHorizontalScroll = false;
|
||||||
|
|
||||||
QPoint _trippleClickPoint;
|
QPoint _trippleClickPoint;
|
||||||
base::Timer _trippleClickTimer;
|
base::Timer _trippleClickTimer;
|
||||||
|
|
|
@ -317,6 +317,9 @@ public:
|
||||||
[[nodiscard]] bool isFakeBotAbout() const {
|
[[nodiscard]] bool isFakeBotAbout() const {
|
||||||
return _flags & MessageFlag::FakeBotAbout;
|
return _flags & MessageFlag::FakeBotAbout;
|
||||||
}
|
}
|
||||||
|
[[nodiscard]] bool showSimilarChannels() const {
|
||||||
|
return _flags & MessageFlag::ShowSimilarChannels;
|
||||||
|
}
|
||||||
[[nodiscard]] bool isRegular() const;
|
[[nodiscard]] bool isRegular() const;
|
||||||
[[nodiscard]] bool isUploading() const;
|
[[nodiscard]] bool isUploading() const;
|
||||||
void sendFailed();
|
void sendFailed();
|
||||||
|
|
|
@ -557,7 +557,7 @@ not_null<HistoryItem*> GenerateJoinedMessage(
|
||||||
bool viaRequest) {
|
bool viaRequest) {
|
||||||
return history->makeMessage(
|
return history->makeMessage(
|
||||||
history->owner().nextLocalMessageId(),
|
history->owner().nextLocalMessageId(),
|
||||||
MessageFlag::Local,
|
MessageFlag::Local | MessageFlag::ShowSimilarChannels,
|
||||||
inviteDate,
|
inviteDate,
|
||||||
GenerateJoinedText(history, inviter, viaRequest));
|
GenerateJoinedText(history, inviter, viaRequest));
|
||||||
}
|
}
|
||||||
|
|
|
@ -135,6 +135,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "ui/chat/chat_theme.h"
|
#include "ui/chat/chat_theme.h"
|
||||||
#include "ui/chat/chat_style.h"
|
#include "ui/chat/chat_style.h"
|
||||||
#include "ui/chat/continuous_scroll.h"
|
#include "ui/chat/continuous_scroll.h"
|
||||||
|
#include "ui/widgets/elastic_scroll.h"
|
||||||
#include "ui/widgets/popup_menu.h"
|
#include "ui/widgets/popup_menu.h"
|
||||||
#include "ui/item_text_options.h"
|
#include "ui/item_text_options.h"
|
||||||
#include "main/main_session.h"
|
#include "main/main_session.h"
|
||||||
|
@ -271,6 +272,15 @@ HistoryWidget::HistoryWidget(
|
||||||
update();
|
update();
|
||||||
}, lifetime());
|
}, 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(
|
_scroll->scrolls(
|
||||||
) | rpl::start_with_next([=] {
|
) | rpl::start_with_next([=] {
|
||||||
handleScroll();
|
handleScroll();
|
||||||
|
|
|
@ -24,8 +24,8 @@ TextState::TextState(
|
||||||
? CursorState::Text
|
? CursorState::Text
|
||||||
: CursorState::None)
|
: CursorState::None)
|
||||||
, link(state.link)
|
, link(state.link)
|
||||||
, afterSymbol(state.afterSymbol)
|
, symbol(state.symbol)
|
||||||
, symbol(state.symbol) {
|
, afterSymbol(state.afterSymbol) {
|
||||||
}
|
}
|
||||||
|
|
||||||
TextState::TextState(
|
TextState::TextState(
|
||||||
|
@ -59,8 +59,8 @@ TextState::TextState(
|
||||||
? CursorState::Text
|
? CursorState::Text
|
||||||
: CursorState::None)
|
: CursorState::None)
|
||||||
, link(state.link)
|
, link(state.link)
|
||||||
, afterSymbol(state.afterSymbol)
|
, symbol(state.symbol)
|
||||||
, symbol(state.symbol) {
|
, afterSymbol(state.afterSymbol) {
|
||||||
}
|
}
|
||||||
|
|
||||||
TextState::TextState(std::nullptr_t, ClickHandlerPtr link)
|
TextState::TextState(std::nullptr_t, ClickHandlerPtr link)
|
||||||
|
|
|
@ -50,10 +50,11 @@ struct TextState {
|
||||||
FullMsgId itemId;
|
FullMsgId itemId;
|
||||||
CursorState cursor = CursorState::None;
|
CursorState cursor = CursorState::None;
|
||||||
ClickHandlerPtr link;
|
ClickHandlerPtr link;
|
||||||
bool overMessageText = false;
|
|
||||||
bool afterSymbol = false;
|
|
||||||
bool customTooltip = false;
|
|
||||||
uint16 symbol = 0;
|
uint16 symbol = 0;
|
||||||
|
bool afterSymbol = false;
|
||||||
|
bool overMessageText = false;
|
||||||
|
bool customTooltip = false;
|
||||||
|
bool horizontalScroll = false;
|
||||||
QString customTooltipText;
|
QString customTooltipText;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "history/view/history_view_message.h"
|
#include "history/view/history_view_message.h"
|
||||||
#include "history/view/media/history_view_media.h"
|
#include "history/view/media/history_view_media.h"
|
||||||
#include "history/view/media/history_view_media_grouped.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_sticker.h"
|
||||||
#include "history/view/media/history_view_large_emoji.h"
|
#include "history/view/media/history_view_large_emoji.h"
|
||||||
#include "history/view/media/history_view_custom_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 "window/window_session_controller.h"
|
||||||
#include "ui/effects/path_shift_gradient.h"
|
#include "ui/effects/path_shift_gradient.h"
|
||||||
#include "ui/effects/reaction_fly_animation.h"
|
#include "ui/effects/reaction_fly_animation.h"
|
||||||
|
#include "ui/effects/ripple_animation.h"
|
||||||
#include "ui/chat/chat_style.h"
|
#include "ui/chat/chat_style.h"
|
||||||
#include "ui/toast/toast.h"
|
#include "ui/toast/toast.h"
|
||||||
#include "ui/text/text_options.h"
|
#include "ui/text/text_options.h"
|
||||||
|
@ -723,6 +725,8 @@ void Element::refreshMedia(Element *replacing) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_media = media->createView(this, replacing);
|
_media = media->createView(this, replacing);
|
||||||
|
} else if (item->showSimilarChannels()) {
|
||||||
|
_media = std::make_unique<SimilarChannels>(this);
|
||||||
} else if (isOnlyCustomEmoji()
|
} else if (isOnlyCustomEmoji()
|
||||||
&& Core::App().settings().largeEmoji()
|
&& Core::App().settings().largeEmoji()
|
||||||
&& !item->isSponsored()) {
|
&& !item->isSponsored()) {
|
||||||
|
|
|
@ -515,6 +515,7 @@ public:
|
||||||
const Reactions::InlineList &reactions) const;
|
const Reactions::InlineList &reactions) const;
|
||||||
void clearCustomEmojiRepaint() const;
|
void clearCustomEmojiRepaint() const;
|
||||||
void hideSpoilers();
|
void hideSpoilers();
|
||||||
|
void repaint() const;
|
||||||
|
|
||||||
[[nodiscard]] ClickHandlerPtr fromPhotoLink() const {
|
[[nodiscard]] ClickHandlerPtr fromPhotoLink() const {
|
||||||
return fromLink();
|
return fromLink();
|
||||||
|
@ -531,6 +532,10 @@ public:
|
||||||
|
|
||||||
void overrideMedia(std::unique_ptr<Media> media);
|
void overrideMedia(std::unique_ptr<Media> media);
|
||||||
|
|
||||||
|
virtual bool consumeHorizontalScroll(QPoint position, int delta) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
virtual ~Element();
|
virtual ~Element();
|
||||||
|
|
||||||
static void Hovered(Element *view);
|
static void Hovered(Element *view);
|
||||||
|
@ -546,8 +551,6 @@ public:
|
||||||
static void ClearGlobal();
|
static void ClearGlobal();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void repaint() const;
|
|
||||||
|
|
||||||
void paintHighlight(
|
void paintHighlight(
|
||||||
Painter &p,
|
Painter &p,
|
||||||
const PaintContext &context,
|
const PaintContext &context,
|
||||||
|
|
|
@ -52,10 +52,8 @@ void ValidateBackgroundEmoji(
|
||||||
}
|
}
|
||||||
const auto tag = Data::CustomEmojiSizeTag::Isolated;
|
const auto tag = Data::CustomEmojiSizeTag::Isolated;
|
||||||
if (!data->emoji) {
|
if (!data->emoji) {
|
||||||
|
const auto repaint = crl::guard(view, [=] { view->repaint(); });
|
||||||
const auto owner = &view->history()->owner();
|
const auto owner = &view->history()->owner();
|
||||||
const auto repaint = crl::guard(view, [=] {
|
|
||||||
view->history()->owner().requestViewRepaint(view);
|
|
||||||
});
|
|
||||||
data->emoji = owner->customEmojiManager().create(
|
data->emoji = owner->customEmojiManager().create(
|
||||||
backgroundEmojiId,
|
backgroundEmojiId,
|
||||||
repaint,
|
repaint,
|
||||||
|
@ -779,7 +777,7 @@ void Reply::createRippleAnimation(
|
||||||
Ui::RippleAnimation::RoundRectMask(
|
Ui::RippleAnimation::RoundRectMask(
|
||||||
size,
|
size,
|
||||||
st::messageQuoteStyle.radius),
|
st::messageQuoteStyle.radius),
|
||||||
[=] { view->history()->owner().requestViewRepaint(view); });
|
[=] { view->repaint(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
void Reply::saveRipplePoint(QPoint point) const {
|
void Reply::saveRipplePoint(QPoint point) const {
|
||||||
|
|
|
@ -411,6 +411,13 @@ QRect Service::innerGeometry() const {
|
||||||
return countGeometry();
|
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 {
|
QRect Service::countGeometry() const {
|
||||||
auto result = QRect(0, 0, width(), height());
|
auto result = QRect(0, 0, width(), height());
|
||||||
if (delegate()->elementIsChatWide()) {
|
if (delegate()->elementIsChatWide()) {
|
||||||
|
@ -429,7 +436,8 @@ QSize Service::performCountCurrentSize(int newWidth) {
|
||||||
return { newWidth, newHeight };
|
return { newWidth, newHeight };
|
||||||
}
|
}
|
||||||
const auto media = this->media();
|
const auto media = this->media();
|
||||||
if (media && media->hideServiceText()) {
|
const auto mediaDisplayed = media && media->isDisplayed();
|
||||||
|
if (mediaDisplayed && media->hideServiceText()) {
|
||||||
newHeight += st::msgServiceMargin.top()
|
newHeight += st::msgServiceMargin.top()
|
||||||
+ media->resizeGetHeight(newWidth)
|
+ media->resizeGetHeight(newWidth)
|
||||||
+ st::msgServiceMargin.bottom();
|
+ st::msgServiceMargin.bottom();
|
||||||
|
@ -448,8 +456,10 @@ QSize Service::performCountCurrentSize(int newWidth) {
|
||||||
? minHeight()
|
? minHeight()
|
||||||
: textHeightFor(nwidth);
|
: textHeightFor(nwidth);
|
||||||
newHeight += st::msgServicePadding.top() + st::msgServicePadding.bottom() + st::msgServiceMargin.top() + st::msgServiceMargin.bottom();
|
newHeight += st::msgServicePadding.top() + st::msgServicePadding.bottom() + st::msgServiceMargin.top() + st::msgServiceMargin.bottom();
|
||||||
if (media) {
|
if (mediaDisplayed) {
|
||||||
newHeight += st::msgServiceMargin.top() + media->resizeGetHeight(media->maxWidth());
|
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());
|
p.setTextPalette(st->serviceTextPalette());
|
||||||
|
|
||||||
const auto media = this->media();
|
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 (!onlyMedia) {
|
||||||
if (media) {
|
if (mediaDisplayed) {
|
||||||
height -= margin.top() + media->height();
|
height -= margin.top() + media->height();
|
||||||
}
|
}
|
||||||
const auto trect = QRect(g.left(), margin.top(), g.width(), 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,
|
.fullWidthSelection = false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (media) {
|
if (mediaDisplayed) {
|
||||||
const auto left = margin.left() + (g.width() - media->maxWidth()) / 2;
|
const auto left = margin.left() + (g.width() - media->width()) / 2;
|
||||||
const auto top = margin.top() + (onlyMedia ? 0 : (height + margin.top()));
|
const auto top = margin.top() + (onlyMedia ? 0 : (height + margin.top()));
|
||||||
p.translate(left, top);
|
p.translate(left, top);
|
||||||
media->draw(p, context.translated(-left, -top).withSelection({}));
|
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 {
|
PointState Service::pointState(QPoint point) const {
|
||||||
const auto media = this->media();
|
const auto media = this->media();
|
||||||
|
const auto mediaDisplayed = media && media->isDisplayed();
|
||||||
|
|
||||||
auto g = countGeometry();
|
auto g = countGeometry();
|
||||||
if (g.width() < 1 || isHidden()) {
|
if (g.width() < 1 || isHidden()) {
|
||||||
|
@ -588,7 +600,7 @@ PointState Service::pointState(QPoint point) const {
|
||||||
if (const auto bar = Get<UnreadBar>()) {
|
if (const auto bar = Get<UnreadBar>()) {
|
||||||
g.setTop(g.top() + bar->height());
|
g.setTop(g.top() + bar->height());
|
||||||
}
|
}
|
||||||
if (media) {
|
if (mediaDisplayed) {
|
||||||
const auto centerPadding = (g.width() - media->width()) / 2;
|
const auto centerPadding = (g.width() - media->width()) / 2;
|
||||||
const auto r = g - QMargins(centerPadding, 0, centerPadding, 0);
|
const auto r = g - QMargins(centerPadding, 0, centerPadding, 0);
|
||||||
if (!r.contains(point)) {
|
if (!r.contains(point)) {
|
||||||
|
@ -602,7 +614,8 @@ PointState Service::pointState(QPoint point) const {
|
||||||
TextState Service::textState(QPoint point, StateRequest request) const {
|
TextState Service::textState(QPoint point, StateRequest request) const {
|
||||||
const auto item = data();
|
const auto item = data();
|
||||||
const auto media = this->media();
|
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);
|
auto result = TextState(item);
|
||||||
|
|
||||||
|
@ -622,8 +635,8 @@ TextState Service::textState(QPoint point, StateRequest request) const {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onlyMedia) {
|
if (onlyMedia) {
|
||||||
return media->textState(point - QPoint(st::msgServiceMargin.left() + (g.width() - media->maxWidth()) / 2, st::msgServiceMargin.top()), request);
|
return media->textState(point - QPoint(st::msgServiceMargin.left() + (g.width() - media->width()) / 2, st::msgServiceMargin.top()), request);
|
||||||
} else if (media) {
|
} else if (mediaDisplayed) {
|
||||||
g.setHeight(g.height() - (st::msgServiceMargin.top() + media->height()));
|
g.setHeight(g.height() - (st::msgServiceMargin.top() + media->height()));
|
||||||
}
|
}
|
||||||
auto trect = g.marginsAdded(-st::msgServicePadding);
|
auto trect = g.marginsAdded(-st::msgServicePadding);
|
||||||
|
@ -656,8 +669,8 @@ TextState Service::textState(QPoint point, StateRequest request) const {
|
||||||
result.link = same->lnk;
|
result.link = same->lnk;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (media) {
|
} else if (mediaDisplayed) {
|
||||||
result = media->textState(point - QPoint(st::msgServiceMargin.left() + (g.width() - media->maxWidth()) / 2, st::msgServiceMargin.top() + g.height() + st::msgServiceMargin.top()), request);
|
result = media->textState(point - QPoint(st::msgServiceMargin.left() + (g.width() - media->width()) / 2, st::msgServiceMargin.top() + g.height() + st::msgServiceMargin.top()), request);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,8 @@ public:
|
||||||
|
|
||||||
QRect innerGeometry() const override;
|
QRect innerGeometry() const override;
|
||||||
|
|
||||||
|
bool consumeHorizontalScroll(QPoint position, int delta) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
[[nodiscard]] QRect countGeometry() const;
|
[[nodiscard]] QRect countGeometry() const;
|
||||||
|
|
||||||
|
|
|
@ -413,13 +413,12 @@ void Game::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) {
|
||||||
if (!_ripple) {
|
if (!_ripple) {
|
||||||
const auto full = QRect(0, 0, width(), height());
|
const auto full = QRect(0, 0, width(), height());
|
||||||
const auto outer = full.marginsRemoved(inBubblePadding());
|
const auto outer = full.marginsRemoved(inBubblePadding());
|
||||||
const auto owner = &parent()->history()->owner();
|
|
||||||
_ripple = std::make_unique<Ui::RippleAnimation>(
|
_ripple = std::make_unique<Ui::RippleAnimation>(
|
||||||
st::defaultRippleAnimation,
|
st::defaultRippleAnimation,
|
||||||
Ui::RippleAnimation::RoundRectMask(
|
Ui::RippleAnimation::RoundRectMask(
|
||||||
outer.size(),
|
outer.size(),
|
||||||
_st.radius),
|
_st.radius),
|
||||||
[=] { owner->requestViewRepaint(parent()); });
|
[=] { repaint(); });
|
||||||
}
|
}
|
||||||
_ripple->add(_lastPoint);
|
_ripple->add(_lastPoint);
|
||||||
} else if (_ripple) {
|
} else if (_ripple) {
|
||||||
|
|
|
@ -14,7 +14,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "data/data_channel.h"
|
#include "data/data_channel.h"
|
||||||
#include "data/data_document.h"
|
#include "data/data_document.h"
|
||||||
#include "data/data_media_types.h"
|
#include "data/data_media_types.h"
|
||||||
#include "data/data_session.h"
|
|
||||||
#include "dialogs/ui/dialogs_stories_content.h"
|
#include "dialogs/ui/dialogs_stories_content.h"
|
||||||
#include "dialogs/ui/dialogs_stories_list.h"
|
#include "dialogs/ui/dialogs_stories_list.h"
|
||||||
#include "history/history.h"
|
#include "history/history.h"
|
||||||
|
@ -348,9 +347,7 @@ void Giveaway::paintChannels(
|
||||||
const auto &thumbnail = channel.thumbnail;
|
const auto &thumbnail = channel.thumbnail;
|
||||||
const auto &geometry = channel.geometry;
|
const auto &geometry = channel.geometry;
|
||||||
if (!_subscribedToThumbnails) {
|
if (!_subscribedToThumbnails) {
|
||||||
thumbnail->subscribeToUpdates([view = parent()] {
|
thumbnail->subscribeToUpdates([=] { repaint(); });
|
||||||
view->history()->owner().requestViewRepaint(view);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto colorIndex = channel.colorIndex;
|
const auto colorIndex = channel.colorIndex;
|
||||||
|
@ -487,13 +484,12 @@ void Giveaway::clickHandlerPressedChanged(
|
||||||
}
|
}
|
||||||
if (pressed) {
|
if (pressed) {
|
||||||
if (!channel.ripple) {
|
if (!channel.ripple) {
|
||||||
const auto owner = &parent()->history()->owner();
|
|
||||||
channel.ripple = std::make_unique<Ui::RippleAnimation>(
|
channel.ripple = std::make_unique<Ui::RippleAnimation>(
|
||||||
st::defaultRippleAnimation,
|
st::defaultRippleAnimation,
|
||||||
Ui::RippleAnimation::RoundRectMask(
|
Ui::RippleAnimation::RoundRectMask(
|
||||||
channel.geometry.size(),
|
channel.geometry.size(),
|
||||||
channel.geometry.height() / 2),
|
channel.geometry.height() / 2),
|
||||||
[=] { owner->requestViewRepaint(parent()); });
|
[=] { repaint(); });
|
||||||
}
|
}
|
||||||
channel.ripple->add(_lastPoint - channel.geometry.topLeft());
|
channel.ripple->add(_lastPoint - channel.geometry.topLeft());
|
||||||
} else if (channel.ripple) {
|
} else if (channel.ripple) {
|
||||||
|
|
|
@ -198,10 +198,6 @@ SelectedQuote Media::selectedQuote(TextSelection selection) const {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Media::isDisplayed() const {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
QSize Media::countCurrentSize(int newWidth) {
|
QSize Media::countCurrentSize(int newWidth) {
|
||||||
return QSize(qMin(newWidth, maxWidth()), minHeight());
|
return QSize(qMin(newWidth, maxWidth()), minHeight());
|
||||||
}
|
}
|
||||||
|
@ -285,7 +281,7 @@ void Media::fillImageSpoiler(
|
||||||
|
|
||||||
void Media::createSpoilerLink(not_null<MediaSpoiler*> spoiler) {
|
void Media::createSpoilerLink(not_null<MediaSpoiler*> spoiler) {
|
||||||
const auto weak = base::make_weak(this);
|
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 ClickContext &context) {
|
||||||
const auto button = context.button;
|
const auto button = context.button;
|
||||||
const auto media = weak.get();
|
const auto media = weak.get();
|
||||||
|
@ -295,15 +291,15 @@ void Media::createSpoilerLink(not_null<MediaSpoiler*> spoiler) {
|
||||||
const auto view = media->parent();
|
const auto view = media->parent();
|
||||||
spoiler->revealed = true;
|
spoiler->revealed = true;
|
||||||
spoiler->revealAnimation.start([=] {
|
spoiler->revealAnimation.start([=] {
|
||||||
media->history()->owner().requestViewRepaint(view);
|
view->repaint();
|
||||||
}, 0., 1., st::fadeWrapDuration);
|
}, 0., 1., st::fadeWrapDuration);
|
||||||
media->history()->owner().requestViewRepaint(view);
|
view->repaint();
|
||||||
media->history()->owner().registerShownSpoiler(view);
|
media->history()->owner().registerShownSpoiler(view);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Media::repaint() const {
|
void Media::repaint() const {
|
||||||
history()->owner().requestViewRepaint(_parent);
|
_parent->repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
Ui::Text::String Media::createCaption(not_null<HistoryItem*> item) const {
|
Ui::Text::String Media::createCaption(not_null<HistoryItem*> item) const {
|
||||||
|
|
|
@ -96,7 +96,9 @@ public:
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] virtual bool isDisplayed() const;
|
[[nodiscard]] virtual bool isDisplayed() const {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
virtual void updateNeedBubbleState() {
|
virtual void updateNeedBubbleState() {
|
||||||
}
|
}
|
||||||
[[nodiscard]] virtual bool hasTextForCopy() const {
|
[[nodiscard]] virtual bool hasTextForCopy() const {
|
||||||
|
@ -335,6 +337,10 @@ public:
|
||||||
virtual void parentTextUpdated() {
|
virtual void parentTextUpdated() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
virtual bool consumeHorizontalScroll(QPoint position, int delta) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
virtual ~Media() = default;
|
virtual ~Media() = default;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -977,3 +977,23 @@ chatGiveawayCountriesSkip: 16px;
|
||||||
chatGiveawayDateTop: 6px;
|
chatGiveawayDateTop: 6px;
|
||||||
chatGiveawayDateSkip: 4px;
|
chatGiveawayDateSkip: 4px;
|
||||||
chatGiveawayBottomSkip: 16px;
|
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;
|
||||||
|
|
Loading…
Add table
Reference in a new issue