init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
/*
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
*/
using "ui/basic.style";
LevelShape {
icon: icon;
position: point;
}
levelStyle: TextStyle(defaultTextStyle) {
font: font(9px semibold);
}
levelTextFg: windowFgActive;
levelMargin: margins(4px, 3px, 2px, 2px);
levelBase: LevelShape {
position: point(0px, 6px);
}
level1: LevelShape(levelBase) {
position: point(-1px, 6px);
icon: icon {{ "levels/level1_inner-26x26", windowBgActive }};
}
level2: LevelShape(level1) {
icon: icon {{ "levels/level2_inner-26x26", windowBgActive }};
}
level3: LevelShape(level1) {
icon: icon {{ "levels/level3_inner-26x26", windowBgActive }};
}
level4: LevelShape(level1) {
icon: icon {{ "levels/level4_inner-26x26", windowBgActive }};
}
level5: LevelShape(level1) {
icon: icon {{ "levels/level5_inner-26x26", windowBgActive }};
}
level6: LevelShape(level1) {
icon: icon {{ "levels/level6_inner-26x26", windowBgActive }};
}
level7: LevelShape(level1) {
icon: icon {{ "levels/level7_inner-26x26", windowBgActive }};
}
level8: LevelShape(level1) {
icon: icon {{ "levels/level8_inner-26x26", windowBgActive }};
}
level9: LevelShape(level1) {
icon: icon {{ "levels/level9_inner-26x26", windowBgActive }};
}
level10: LevelShape(levelBase) {
icon: icon {{ "levels/level10_inner-26x26", windowBgActive }};
}
level20: LevelShape(levelBase) {
icon: icon {{ "levels/level20_inner-26x26", windowBgActive }};
}
level30: LevelShape(levelBase) {
icon: icon {{ "levels/level30_inner-26x26", windowBgActive }};
}
level40: LevelShape(levelBase) {
icon: icon {{ "levels/level40_inner-26x26", windowBgActive }};
}
level50: LevelShape(levelBase) {
icon: icon {{ "levels/level50_inner-26x26", windowBgActive }};
}
level60: LevelShape(levelBase) {
icon: icon {{ "levels/level60_inner-26x26", windowBgActive }};
}
level70: LevelShape(levelBase) {
icon: icon {{ "levels/level70_inner-26x26", windowBgActive }};
}
level80: LevelShape(levelBase) {
icon: icon {{ "levels/level80_inner-26x26", windowBgActive }};
}
level90: LevelShape(levelBase) {
icon: icon {{ "levels/level90_inner-26x26", windowBgActive }};
}
levelNegative: LevelShape(levelBase) {
icon: icon {{ "levels/level_warning-18x18", attentionButtonFg, point(6px, 5px) }};
}
levelNegativeBubble: icon {{ "levels/level_warning-28x28", windowFgActive }};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,81 @@
/*
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 "base/object_ptr.h"
namespace Ui {
class RpWidget;
class VerticalLayout;
class MultiSlideTracker;
} // namespace Ui
namespace Data {
class ForumTopic;
class SavedSublist;
} // namespace Data
namespace Info {
class Controller;
} // namespace Info
namespace Info::Profile {
extern const char kOptionShowPeerIdBelowAbout[];
extern const char kOptionShowChannelJoinedBelowAbout[];
class Cover;
struct Origin;
object_ptr<Ui::RpWidget> SetupDetails(
not_null<Controller*> controller,
not_null<Ui::RpWidget*> parent,
not_null<PeerData*> peer,
Origin origin,
Ui::MultiSlideTracker &mainTracker);
object_ptr<Ui::RpWidget> SetupDetails(
not_null<Controller*> controller,
not_null<Ui::RpWidget*> parent,
not_null<Data::ForumTopic*> topic,
Ui::MultiSlideTracker &mainTracker);
object_ptr<Ui::RpWidget> SetupDetails(
not_null<Controller*> controller,
not_null<Ui::RpWidget*> parent,
not_null<Data::SavedSublist*> sublist,
Ui::MultiSlideTracker &mainTracker);
object_ptr<Ui::RpWidget> SetupActions(
not_null<Controller*> controller,
not_null<Ui::RpWidget*> parent,
not_null<PeerData*> peer);
object_ptr<Ui::RpWidget> SetupChannelMembersAndManage(
not_null<Controller*> controller,
not_null<Ui::RpWidget*> parent,
not_null<PeerData*> peer);
Cover *AddCover(
not_null<Ui::VerticalLayout*> container,
not_null<Controller*> controller,
not_null<PeerData*> peer,
Data::ForumTopic *topic,
Data::SavedSublist *sublist);
void AddDetails(
not_null<Ui::VerticalLayout*> container,
not_null<Controller*> controller,
not_null<PeerData*> peer,
Data::ForumTopic *topic,
Data::SavedSublist *sublist,
Origin origin,
Ui::MultiSlideTracker &mainTracker,
rpl::variable<bool> &dividerOverridden);
} // namespace Info::Profile

View File

@@ -0,0 +1,299 @@
/*
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 "info/profile/info_profile_badge.h"
#include "data/data_changes.h"
#include "data/data_emoji_statuses.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "data/stickers/data_custom_emoji.h"
#include "info/profile/info_profile_values.h"
#include "info/profile/info_profile_emoji_status_panel.h"
#include "lang/lang_keys.h"
#include "ui/widgets/buttons.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "main/main_session.h"
#include "styles/style_info.h"
namespace Info::Profile {
namespace {
[[nodiscard]] bool HasPremiumClick(const Badge::Content &content) {
return content.badge == BadgeType::Premium
|| (content.badge == BadgeType::Verified && content.emojiStatusId);
}
} // namespace
Badge::Badge(
not_null<QWidget*> parent,
const style::InfoPeerBadge &st,
not_null<Main::Session*> session,
rpl::producer<Content> content,
EmojiStatusPanel *emojiStatusPanel,
Fn<bool()> animationPaused,
int customStatusLoopsLimit,
base::flags<BadgeType> allowed)
: _parent(parent)
, _st(st)
, _session(session)
, _emojiStatusPanel(emojiStatusPanel)
, _customStatusLoopsLimit(customStatusLoopsLimit)
, _allowed(allowed)
, _animationPaused(std::move(animationPaused)) {
std::move(
content
) | rpl::on_next([=](Content content) {
setContent(content);
}, _lifetime);
}
Badge::~Badge() = default;
Ui::RpWidget *Badge::widget() const {
return _view.data();
}
void Badge::setContent(Content content) {
if (!(_allowed & content.badge)
|| (!_session->premiumBadgesShown()
&& content.badge == BadgeType::Premium)) {
content.badge = BadgeType::None;
}
if (!(_allowed & content.badge)) {
content.badge = BadgeType::None;
}
if (_content == content) {
return;
}
_content = content;
_emojiStatus = nullptr;
_view.destroy();
if (_content.badge == BadgeType::None) {
_updated.fire({});
return;
}
_view.create(_parent);
_view->show();
switch (_content.badge) {
case BadgeType::Verified:
case BadgeType::BotVerified:
case BadgeType::Premium: {
const auto id = _content.emojiStatusId;
const auto emoji = id
? (Data::FrameSizeFromTag(sizeTag())
/ style::DevicePixelRatio())
: 0;
const auto &style = st();
const auto icon = (_content.badge == BadgeType::Verified)
? &style.verified
: id
? nullptr
: &style.premium;
const auto iconForeground = (_content.badge == BadgeType::Verified)
? &style.verifiedCheck
: nullptr;
if (id) {
_emojiStatus = _session->data().customEmojiManager().create(
Data::EmojiStatusCustomId(id),
[raw = _view.data()] { raw->update(); },
sizeTag());
if (_content.badge == BadgeType::BotVerified) {
_emojiStatus = std::make_unique<Ui::Text::FirstFrameEmoji>(
std::move(_emojiStatus));
} else if (_customStatusLoopsLimit > 0) {
_emojiStatus = std::make_unique<Ui::Text::LimitedLoopsEmoji>(
std::move(_emojiStatus),
_customStatusLoopsLimit);
}
}
const auto width = emoji + (icon ? icon->width() : 0);
const auto height = std::max(emoji, icon ? icon->height() : 0);
_view->resize(width, height);
_view->paintRequest(
) | rpl::on_next([=, check = _view.data()]{
if (_emojiStatus) {
auto args = Ui::Text::CustomEmoji::Context{
.textColor = style.premiumFg->c,
.now = crl::now(),
.paused = ((_animationPaused && _animationPaused())
|| On(PowerSaving::kEmojiStatus)),
};
if (!_emojiStatusPanel
|| !_emojiStatusPanel->paintBadgeFrame(check)) {
Painter p(check);
_emojiStatus->paint(p, args);
}
}
if (icon) {
auto p = Painter(check);
if (_overrideSt && !iconForeground) {
icon->paint(
p,
emoji,
0,
check->width(),
_overrideSt->premiumFg->c);
} else {
icon->paint(p, emoji, 0, check->width());
}
if (iconForeground) {
if (_overrideSt) {
iconForeground->paint(
p,
emoji,
0,
check->width(),
_overrideSt->premiumFg->c);
} else {
iconForeground->paint(p, emoji, 0, check->width());
}
}
}
}, _view->lifetime());
} break;
case BadgeType::Scam:
case BadgeType::Fake:
case BadgeType::Direct: {
const auto type = (_content.badge == BadgeType::Direct)
? Ui::TextBadgeType::Direct
: (_content.badge == BadgeType::Fake)
? Ui::TextBadgeType::Fake
: Ui::TextBadgeType::Scam;
const auto size = Ui::TextBadgeSize(type);
const auto skip = st::infoVerifiedCheckPosition.x();
_view->resize(
size.width() + 2 * skip,
size.height() + 2 * skip);
_view->paintRequest(
) | rpl::on_next([=, badge = _view.data()]{
Painter p(badge);
Ui::DrawTextBadge(
type,
p,
badge->rect().marginsRemoved({ skip, skip, skip, skip }),
badge->width(),
(type == Ui::TextBadgeType::Direct
? st::windowSubTextFg
: st::attentionButtonFg));
}, _view->lifetime());
} break;
}
if (!HasPremiumClick(_content) || !_premiumClickCallback) {
_view->setAttribute(Qt::WA_TransparentForMouseEvents);
} else {
_view->setClickedCallback(_premiumClickCallback);
}
_updated.fire({});
}
void Badge::setPremiumClickCallback(Fn<void()> callback) {
_premiumClickCallback = std::move(callback);
if (_view && HasPremiumClick(_content)) {
if (!_premiumClickCallback) {
_view->setAttribute(Qt::WA_TransparentForMouseEvents);
} else {
_view->setAttribute(Qt::WA_TransparentForMouseEvents, false);
_view->setClickedCallback(_premiumClickCallback);
}
}
}
void Badge::setOverrideStyle(const style::InfoPeerBadge *st) {
const auto was = _content;
_overrideSt = st;
_content = {};
setContent(was);
}
rpl::producer<> Badge::updated() const {
return _updated.events();
}
void Badge::move(int left, int top, int bottom) {
if (!_view) {
return;
}
const auto &style = st();
const auto star = !_emojiStatus
&& (_content.badge == BadgeType::Premium
|| _content.badge == BadgeType::Verified);
const auto fake = !_emojiStatus && !star;
const auto skip = fake ? 0 : style.position.x();
const auto badgeLeft = left + skip;
const auto badgeTop = top
+ (star
? style.position.y()
: (bottom - top - _view->height()) / 2);
_view->moveToLeft(badgeLeft, badgeTop);
}
const style::InfoPeerBadge &Badge::st() const {
return _overrideSt ? *_overrideSt : _st;
}
Data::CustomEmojiSizeTag Badge::sizeTag() const {
using SizeTag = Data::CustomEmojiSizeTag;
const auto &style = st();
return (style.sizeTag == 2)
? SizeTag::Isolated
: (style.sizeTag == 1)
? SizeTag::Large
: SizeTag::Normal;
}
rpl::producer<Badge::Content> BadgeContentForPeer(not_null<PeerData*> peer) {
const auto statusOnlyForPremium = peer->isUser();
return rpl::combine(
BadgeValue(peer),
EmojiStatusIdValue(peer)
) | rpl::map([=](BadgeType badge, EmojiStatusId emojiStatusId) {
if (emojiStatusId.collectible) {
return Badge::Content{ BadgeType::Premium, emojiStatusId };
}
if (badge == BadgeType::Verified) {
badge = BadgeType::None;
}
if (statusOnlyForPremium && badge != BadgeType::Premium) {
emojiStatusId = EmojiStatusId();
} else if (emojiStatusId && badge == BadgeType::None) {
badge = BadgeType::Premium;
}
return Badge::Content{ badge, emojiStatusId };
});
}
rpl::producer<Badge::Content> VerifiedContentForPeer(
not_null<PeerData*> peer) {
return BadgeValue(peer) | rpl::map([=](BadgeType badge) {
if (badge != BadgeType::Verified) {
badge = BadgeType::None;
}
return Badge::Content{ badge };
});
}
rpl::producer<Badge::Content> BotVerifyBadgeForPeer(
not_null<PeerData*> peer) {
return peer->session().changes().peerFlagsValue(
peer,
Data::PeerUpdate::Flag::VerifyInfo
) | rpl::map([=] {
const auto info = peer->botVerifyDetails();
return Badge::Content{
.badge = info ? BadgeType::BotVerified : BadgeType::None,
.emojiStatusId = { info ? info->iconId : DocumentId() },
};
});
}
} // namespace Info::Profile

View File

@@ -0,0 +1,107 @@
/*
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 "base/flags.h"
#include "base/object_ptr.h"
namespace style {
struct InfoPeerBadge;
} // namespace style
namespace Data {
enum class CustomEmojiSizeTag : uchar;
} // namespace Data
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class RpWidget;
class AbstractButton;
} // namespace Ui
namespace Ui::Text {
class CustomEmoji;
} // namespace Ui::Text
namespace Info::Profile {
class EmojiStatusPanel;
enum class BadgeType : uchar {
None = 0x00,
Verified = 0x01,
BotVerified = 0x02,
Premium = 0x04,
Scam = 0x08,
Fake = 0x10,
Direct = 0x20,
};
inline constexpr bool is_flag_type(BadgeType) { return true; }
class Badge final {
public:
struct Content {
BadgeType badge = BadgeType::None;
EmojiStatusId emojiStatusId;
friend inline bool operator==(Content, Content) = default;
};
Badge(
not_null<QWidget*> parent,
const style::InfoPeerBadge &st,
not_null<Main::Session*> session,
rpl::producer<Content> content,
EmojiStatusPanel *emojiStatusPanel,
Fn<bool()> animationPaused,
int customStatusLoopsLimit = 0,
base::flags<BadgeType> allowed
= base::flags<BadgeType>::from_raw(-1));
~Badge();
[[nodiscard]] Ui::RpWidget *widget() const;
void setPremiumClickCallback(Fn<void()> callback);
void setOverrideStyle(const style::InfoPeerBadge *st);
[[nodiscard]] rpl::producer<> updated() const;
void move(int left, int top, int bottom);
[[nodiscard]] Data::CustomEmojiSizeTag sizeTag() const;
private:
void setContent(Content content);
[[nodiscard]] const style::InfoPeerBadge &st() const;
const not_null<QWidget*> _parent;
const style::InfoPeerBadge &_st;
const style::InfoPeerBadge *_overrideSt = nullptr;
const not_null<Main::Session*> _session;
EmojiStatusPanel *_emojiStatusPanel = nullptr;
const int _customStatusLoopsLimit = 0;
std::unique_ptr<Ui::Text::CustomEmoji> _emojiStatus;
base::flags<BadgeType> _allowed;
Content _content;
Fn<void()> _premiumClickCallback;
Fn<bool()> _animationPaused;
object_ptr<Ui::AbstractButton> _view = { nullptr };
rpl::event_stream<> _updated;
rpl::lifetime _lifetime;
};
[[nodiscard]] rpl::producer<Badge::Content> BadgeContentForPeer(
not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<Badge::Content> VerifiedContentForPeer(
not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<Badge::Content> BotVerifyBadgeForPeer(
not_null<PeerData*> peer);
} // namespace Info::Profile

View File

@@ -0,0 +1,224 @@
/*
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 "info/profile/info_profile_badge_tooltip.h"
#include "data/data_emoji_statuses.h"
#include "base/event_filter.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "styles/style_info.h"
namespace Info::Profile {
namespace {
constexpr auto kGlareDurationStep = crl::time(320);
constexpr auto kGlareTimeout = crl::time(1000);
} // namespace
BadgeTooltip::BadgeTooltip(
not_null<QWidget*> parent,
std::shared_ptr<Data::EmojiStatusCollectible> collectible,
not_null<QWidget*> pointTo)
: Ui::RpWidget(parent)
, _st(st::infoGiftTooltip)
, _collectible(std::move(collectible))
, _text(_collectible->title)
, _font(st::infoGiftTooltipFont)
, _inner(_font->width(_text), _font->height)
, _outer(_inner.grownBy(_st.padding))
, _stroke(st::lineWidth)
, _skip(2 * _stroke)
, _full(_outer + QSize(2 * _skip, _st.arrow + 2 * _skip))
, _glareSize(_outer.height() * 3)
, _glareRange(_outer.width() + _glareSize)
, _glareDuration(_glareRange * kGlareDurationStep / _glareSize)
, _glareTimer([=] { showGlare(); }) {
resize(_full + QSize(0, _st.shift));
setupGeometry(pointTo);
}
void BadgeTooltip::fade(bool shown) {
if (_shown == shown) {
return;
}
show();
_shown = shown;
_showAnimation.start([=] {
update();
if (!_showAnimation.animating()) {
if (!_shown) {
hide();
} else {
showGlare();
}
}
}, _shown ? 0. : 1., _shown ? 1. : 0., _st.duration, anim::easeInCirc);
}
void BadgeTooltip::showGlare() {
_glareAnimation.start([=] {
update();
if (!_glareAnimation.animating()) {
_glareTimer.callOnce(kGlareTimeout);
}
}, 0., 1., _glareDuration);
}
void BadgeTooltip::finishAnimating() {
_showAnimation.stop();
if (!_shown) {
hide();
}
}
void BadgeTooltip::setOpacity(float64 opacity) {
_opacity = opacity;
update();
}
crl::time BadgeTooltip::glarePeriod() const {
return _glareDuration + kGlareTimeout;
}
void BadgeTooltip::paintEvent(QPaintEvent *e) {
const auto glare = _glareAnimation.value(0.);
_glareRight = anim::interpolate(0, _glareRange, glare);
prepareImage();
auto p = QPainter(this);
const auto shown = _showAnimation.value(_shown ? 1. : 0.);
p.setOpacity(shown * _opacity);
const auto imageHeight = _image.height() / _image.devicePixelRatio();
const auto top = anim::interpolate(0, height() - imageHeight, shown);
p.drawImage(0, top, _image);
}
void BadgeTooltip::setupGeometry(not_null<QWidget*> pointTo) {
auto widget = pointTo.get();
const auto parent = parentWidget();
const auto refresh = [=, weak = base::make_weak(pointTo)] {
const auto strong = weak.get();
if (!strong) {
hide();
return setGeometry({});
}
const auto rect = Ui::MapFrom(parent, pointTo, pointTo->rect());
const auto point = QPoint(rect.center().x(), rect.y());
const auto left = point.x() - (width() / 2);
const auto skip = _st.padding.left();
setGeometry(
std::min(std::max(left, skip), parent->width() - width() - skip),
std::max(point.y() - height() - _st.margin.bottom(), skip),
width(),
height());
const auto arrowMiddle = point.x() - x();
if (_arrowMiddle != arrowMiddle) {
_arrowMiddle = arrowMiddle;
update();
}
};
refresh();
while (widget && widget != parent) {
base::install_event_filter(this, widget, [=](not_null<QEvent*> e) {
if (e->type() == QEvent::Resize
|| e->type() == QEvent::Move
|| e->type() == QEvent::ZOrderChange) {
refresh();
raise();
}
return base::EventFilterResult::Continue;
});
widget = widget->parentWidget();
}
}
void BadgeTooltip::prepareImage() {
const auto ratio = style::DevicePixelRatio();
const auto arrow = _st.arrow;
const auto size = _full * ratio;
if (_image.size() != size) {
_image = QImage(size, QImage::Format_ARGB32_Premultiplied);
_image.setDevicePixelRatio(ratio);
} else if (_imageGlareRight == _glareRight
&& _imageArrowMiddle == _arrowMiddle) {
return;
}
_imageGlareRight = _glareRight;
_imageArrowMiddle = _arrowMiddle;
_image.fill(Qt::transparent);
const auto gfrom = _imageGlareRight - _glareSize;
const auto gtill = _imageGlareRight;
auto path = QPainterPath();
const auto width = _outer.width();
const auto height = _outer.height();
const auto radius = (height + 1) / 2;
const auto diameter = height;
path.moveTo(radius, 0);
path.lineTo(width - radius, 0);
path.arcTo(
QRect(QPoint(width - diameter, 0), QSize(diameter, diameter)),
90,
-180);
const auto xarrow = _arrowMiddle - _skip;
if (xarrow - arrow <= radius || xarrow + arrow >= width - radius) {
path.lineTo(radius, height);
} else {
path.lineTo(xarrow + arrow, height);
path.lineTo(xarrow, height + arrow);
path.lineTo(xarrow - arrow, height);
path.lineTo(radius, height);
}
path.arcTo(
QRect(QPoint(0, 0), QSize(diameter, diameter)),
-90,
-180);
path.closeSubpath();
auto p = QPainter(&_image);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
if (gtill > 0) {
auto gradient = QLinearGradient(gfrom, 0, gtill, 0);
gradient.setStops({
{ 0., _collectible->edgeColor },
{ 0.5, _collectible->centerColor },
{ 1., _collectible->edgeColor },
});
p.setBrush(gradient);
} else {
p.setBrush(_collectible->edgeColor);
}
p.translate(_skip, _skip);
p.drawPath(path);
p.setCompositionMode(QPainter::CompositionMode_Source);
p.setBrush(Qt::NoBrush);
auto copy = _collectible->textColor;
copy.setAlpha(0);
if (gtill > 0) {
auto gradient = QLinearGradient(gfrom, 0, gtill, 0);
gradient.setStops({
{ 0., copy },
{ 0.5, _collectible->textColor },
{ 1., copy },
});
p.setPen(QPen(gradient, _stroke));
} else {
p.setPen(QPen(copy, _stroke));
}
p.drawPath(path);
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
p.setFont(_font);
p.setPen(QColor(255, 255, 255));
p.drawText(_st.padding.left(), _st.padding.top() + _font->ascent, _text);
}
} // namespace Info::Profile

View File

@@ -0,0 +1,70 @@
/*
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 "ui/rp_widget.h"
#include "ui/effects/animations.h"
#include "base/timer.h"
namespace style {
struct ImportantTooltip;
} // namespace style
namespace Data {
struct EmojiStatusCollectible;
} // namespace Data
namespace Info::Profile {
class BadgeTooltip final : public Ui::RpWidget {
public:
BadgeTooltip(
not_null<QWidget*> parent,
std::shared_ptr<Data::EmojiStatusCollectible> collectible,
not_null<QWidget*> pointTo);
void fade(bool shown);
void finishAnimating();
void setOpacity(float64 opacity);
[[nodiscard]] crl::time glarePeriod() const;
private:
void paintEvent(QPaintEvent *e) override;
void setupGeometry(not_null<QWidget*> pointTo);
void prepareImage();
void showGlare();
const style::ImportantTooltip &_st;
std::shared_ptr<Data::EmojiStatusCollectible> _collectible;
QString _text;
const style::font &_font;
QSize _inner;
QSize _outer;
int _stroke = 0;
int _skip = 0;
QSize _full;
int _glareSize = 0;
int _glareRange = 0;
crl::time _glareDuration = 0;
base::Timer _glareTimer;
Ui::Animations::Simple _showAnimation;
Ui::Animations::Simple _glareAnimation;
QImage _image;
int _glareRight = 0;
int _imageGlareRight = 0;
int _arrowMiddle = 0;
int _imageArrowMiddle = 0;
bool _shown = false;
float64 _opacity = 1.;
};
} // namespace Info::Profile

View File

@@ -0,0 +1,851 @@
/*
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 "info/profile/info_profile_cover.h"
#include "api/api_user_privacy.h"
#include "base/timer_rpl.h"
#include "data/data_peer_values.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_emoji_statuses.h"
#include "data/data_peer.h"
#include "data/data_user.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_changes.h"
#include "data/data_session.h"
#include "data/data_forum_topic.h"
#include "data/stickers/data_custom_emoji.h"
#include "info/profile/info_profile_badge.h"
#include "info/profile/info_profile_badge_tooltip.h"
#include "info/profile/info_profile_emoji_status_panel.h"
#include "info/profile/info_profile_status_label.h"
#include "info/profile/info_profile_values.h"
#include "info/info_controller.h"
#include "info/info_memento.h"
#include "boxes/peers/edit_forum_topic_box.h"
#include "boxes/report_messages_box.h"
#include "history/view/media/history_view_sticker_player.h"
#include "lang/lang_keys.h"
#include "ui/boxes/show_or_premium_box.h"
#include "ui/controls/stars_rating.h"
#include "ui/controls/userpic_button.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/popup_menu.h"
#include "ui/text/text_utilities.h"
#include "ui/basic_click_handlers.h"
#include "ui/ui_utility.h"
#include "ui/painter.h"
#include "base/event_filter.h"
#include "base/unixtime.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "settings/settings_premium.h"
#include "chat_helpers/stickers_lottie.h"
#include "apiwrap.h"
#include "api/api_peer_photo.h"
#include "styles/style_boxes.h"
#include "styles/style_info.h"
#include "styles/style_dialogs.h"
#include "styles/style_menu_icons.h"
namespace Info::Profile {
namespace {
constexpr auto kWaitBeforeGiftBadge = crl::time(1000);
constexpr auto kGiftBadgeGlares = 3;
constexpr auto kGlareDurationStep = crl::time(320);
constexpr auto kGlareTimeout = crl::time(1000);
[[nodiscard]] const style::InfoProfileCover &CoverStyle(
not_null<PeerData*> peer,
Data::ForumTopic *topic,
Cover::Role role) {
return (role == Cover::Role::EditContact)
? st::infoEditContactCover
: topic
? st::infoTopicCover
: peer->isMegagroup()
? st::infoProfileMegagroupCover
: st::infoProfileCover;
}
} // namespace
QMargins LargeCustomEmojiMargins() {
const auto ratio = style::DevicePixelRatio();
const auto emoji = Ui::Emoji::GetSizeLarge() / ratio;
const auto size = Data::FrameSizeFromTag(Data::CustomEmojiSizeTag::Large)
/ ratio;
const auto left = (size - emoji) / 2;
const auto right = size - emoji - left;
return { left, left, right, right };
}
TopicIconView::TopicIconView(
not_null<Data::ForumTopic*> topic,
Fn<bool()> paused,
Fn<void()> update)
: TopicIconView(
topic,
std::move(paused),
std::move(update),
st::windowSubTextFg) {
}
TopicIconView::TopicIconView(
not_null<Data::ForumTopic*> topic,
Fn<bool()> paused,
Fn<void()> update,
const style::color &generalIconFg)
: _topic(topic)
, _generalIconFg(generalIconFg)
, _paused(std::move(paused))
, _update(std::move(update)) {
setup(topic);
}
void TopicIconView::paintInRect(QPainter &p, QRect rect) {
const auto paint = [&](const QImage &image) {
const auto size = image.size() / style::DevicePixelRatio();
p.drawImage(
QRect(
rect.x() + (rect.width() - size.width()) / 2,
rect.y() + (rect.height() - size.height()) / 2,
size.width(),
size.height()),
image);
};
if (_player && _player->ready()) {
const auto colored = _playerUsesTextColor
? st::windowFg->c
: QColor(0, 0, 0, 0);
paint(_player->frame(
st::infoTopicCover.photo.size,
colored,
false,
crl::now(),
_paused()).image);
_player->markFrameShown();
} else if (!_topic->iconId() && !_image.isNull()) {
paint(_image);
}
}
void TopicIconView::setup(not_null<Data::ForumTopic*> topic) {
setupPlayer(topic);
setupImage(topic);
}
void TopicIconView::setupPlayer(not_null<Data::ForumTopic*> topic) {
IconIdValue(
topic
) | rpl::map([=](DocumentId id) -> rpl::producer<DocumentData*> {
if (!id) {
return rpl::single((DocumentData*)nullptr);
}
return topic->owner().customEmojiManager().resolve(
id
) | rpl::map([=](not_null<DocumentData*> document) {
return document.get();
}) | rpl::map_error_to_done();
}) | rpl::flatten_latest(
) | rpl::map([=](DocumentData *document)
-> rpl::producer<std::shared_ptr<StickerPlayer>> {
if (!document) {
return rpl::single(std::shared_ptr<StickerPlayer>());
}
const auto media = document->createMediaView();
media->checkStickerLarge();
media->goodThumbnailWanted();
return rpl::single() | rpl::then(
document->session().downloaderTaskFinished()
) | rpl::filter([=] {
return media->loaded();
}) | rpl::take(1) | rpl::map([=] {
auto result = std::shared_ptr<StickerPlayer>();
const auto sticker = document->sticker();
if (sticker->isLottie()) {
result = std::make_shared<HistoryView::LottiePlayer>(
ChatHelpers::LottiePlayerFromDocument(
media.get(),
ChatHelpers::StickerLottieSize::StickerSet,
st::infoTopicCover.photo.size,
Lottie::Quality::High));
} else if (sticker->isWebm()) {
result = std::make_shared<HistoryView::WebmPlayer>(
media->owner()->location(),
media->bytes(),
st::infoTopicCover.photo.size);
} else {
result = std::make_shared<HistoryView::StaticStickerPlayer>(
media->owner()->location(),
media->bytes(),
st::infoTopicCover.photo.size);
}
result->setRepaintCallback(_update);
_playerUsesTextColor = media->owner()->emojiUsesTextColor();
return result;
});
}) | rpl::flatten_latest(
) | rpl::on_next([=](std::shared_ptr<StickerPlayer> player) {
_player = std::move(player);
if (!_player) {
_update();
}
}, _lifetime);
}
void TopicIconView::setupImage(not_null<Data::ForumTopic*> topic) {
using namespace Data;
if (topic->isGeneral()) {
rpl::single(rpl::empty) | rpl::then(
style::PaletteChanged()
) | rpl::on_next([=] {
_image = ForumTopicGeneralIconFrame(
st::infoForumTopicIcon.size,
_generalIconFg->c);
_update();
}, _lifetime);
return;
}
rpl::combine(
TitleValue(topic),
ColorIdValue(topic)
) | rpl::map([=](const QString &title, int32 colorId) {
return ForumTopicIconFrame(colorId, title, st::infoForumTopicIcon);
}) | rpl::on_next([=](QImage &&image) {
_image = std::move(image);
_update();
}, _lifetime);
}
TopicIconButton::TopicIconButton(
QWidget *parent,
not_null<Window::SessionController*> controller,
not_null<Data::ForumTopic*> topic)
: TopicIconButton(parent, topic, [=] {
return controller->isGifPausedAtLeastFor(Window::GifPauseReason::Layer);
}) {
}
TopicIconButton::TopicIconButton(
QWidget *parent,
not_null<Data::ForumTopic*> topic,
Fn<bool()> paused)
: AbstractButton(parent)
, _view(topic, paused, [=] { update(); }) {
resize(st::infoTopicCover.photo.size);
paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(this);
_view.paintInRect(p, rect());
}, lifetime());
}
Cover::Cover(
QWidget *parent,
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer,
Fn<not_null<QWidget*>()> parentForTooltip)
: Cover(
parent,
controller,
peer,
nullptr,
Role::Info,
NameValue(peer),
parentForTooltip) {
}
Cover::Cover(
QWidget *parent,
not_null<Window::SessionController*> controller,
not_null<Data::ForumTopic*> topic)
: Cover(
parent,
controller,
topic->peer(),
topic,
Role::Info,
TitleValue(topic),
nullptr) {
}
Cover::Cover(
QWidget *parent,
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer,
Role role,
rpl::producer<QString> title)
: Cover(
parent,
controller,
peer,
nullptr,
role,
std::move(title),
nullptr) {
}
Cover::Cover(
QWidget *parent,
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer,
Data::ForumTopic *topic,
Role role,
rpl::producer<QString> title,
Fn<not_null<QWidget*>()> parentForTooltip)
: FixedHeightWidget(parent, CoverStyle(peer, topic, role).height)
, _st(CoverStyle(peer, topic, role))
, _role(role)
, _controller(controller)
, _peer(peer)
, _emojiStatusPanel(peer->isSelf()
? std::make_unique<EmojiStatusPanel>()
: nullptr)
, _botVerify(role == Role::EditContact
? nullptr
: std::make_unique<Badge>(
this,
st::infoBotVerifyBadge,
&peer->session(),
BotVerifyBadgeForPeer(peer),
nullptr,
[=] {
return controller->isGifPausedAtLeastFor(
Window::GifPauseReason::Layer);
}))
, _badgeContent(BadgeContentForPeer(peer))
, _badge(role == Role::EditContact
? nullptr
: std::make_unique<Badge>(
this,
st::infoPeerBadge,
&peer->session(),
_badgeContent.value(),
_emojiStatusPanel.get(),
[=] {
return controller->isGifPausedAtLeastFor(
Window::GifPauseReason::Layer);
}))
, _verified(role == Role::EditContact
? nullptr
: std::make_unique<Badge>(
this,
st::infoPeerBadge,
&peer->session(),
VerifiedContentForPeer(peer),
_emojiStatusPanel.get(),
[=] {
return controller->isGifPausedAtLeastFor(
Window::GifPauseReason::Layer);
}))
, _parentForTooltip(std::move(parentForTooltip))
, _badgeTooltipHide([=] { hideBadgeTooltip(); })
, _userpic(topic
? nullptr
: object_ptr<Ui::UserpicButton>(
this,
controller,
_peer->userpicPaintingPeer(),
Ui::UserpicButton::Role::OpenPhoto,
Ui::UserpicButton::Source::PeerPhoto,
_st.photo,
_peer->userpicShape()))
, _changePersonal((role == Role::Info
|| role == Role::EditContact
|| topic
|| !_peer->isUser()
|| _peer->isSelf()
|| _peer->asUser()->isBot())
? nullptr
: CreateUploadSubButton(this, _peer->asUser(), controller).get())
, _iconButton(topic
? object_ptr<TopicIconButton>(this, controller, topic)
: nullptr)
, _name(this, _st.name)
, _starsRating(_peer->isUser() && _role != Role::EditContact
? std::make_unique<Ui::StarsRating>(
this,
_controller->uiShow(),
_peer->isSelf() ? QString() : _peer->shortName(),
Data::StarsRatingValue(_peer),
(_peer->isSelf()
? [=] { return _peer->owner().pendingStarsRating(); }
: Fn<Data::StarsRatingPending()>()))
: nullptr)
, _status(this, _st.status)
, _statusLabel(std::make_unique<StatusLabel>(_status.data(), _peer))
, _showLastSeen(this, tr::lng_status_lastseen_when(), _st.showLastSeen) {
_peer->updateFull();
if (const auto broadcast = _peer->monoforumBroadcast()) {
broadcast->updateFull();
}
_name->setSelectable(true);
_name->setContextCopyText(tr::lng_profile_copy_fullname(tr::now));
if (!_peer->isMegagroup()) {
_status->setAttribute(Qt::WA_TransparentForMouseEvents);
if (const auto rating = _starsRating.get()) {
_statusShift = rating->widthValue();
_statusShift.changes() | rpl::on_next([=] {
refreshStatusGeometry(width());
}, _status->lifetime());
rating->raise();
}
}
setupShowLastSeen();
if (_badge) {
_badge->setPremiumClickCallback([=] {
if (const auto panel = _emojiStatusPanel.get()) {
panel->show(_controller, _badge->widget(), _badge->sizeTag());
} else {
::Settings::ShowEmojiStatusPremium(_controller, _peer);
}
});
}
auto badgeUpdates = rpl::producer<rpl::empty_value>();
if (_badge) {
badgeUpdates = rpl::merge(
std::move(badgeUpdates),
_badge->updated());
}
if (_verified) {
badgeUpdates = rpl::merge(
std::move(badgeUpdates),
_verified->updated());
}
if (_botVerify) {
badgeUpdates = rpl::merge(
std::move(badgeUpdates),
_botVerify->updated());
}
std::move(badgeUpdates) | rpl::on_next([=] {
refreshNameGeometry(width());
}, _name->lifetime());
initViewers(std::move(title));
setupChildGeometry();
setupUniqueBadgeTooltip();
if (_userpic) {
} else if (topic->canEdit()) {
_iconButton->setClickedCallback([=] {
_controller->show(Box(
EditForumTopicBox,
_controller,
topic->history(),
topic->rootId()));
});
} else {
_iconButton->setAttribute(Qt::WA_TransparentForMouseEvents);
}
}
void Cover::setupShowLastSeen() {
const auto user = _peer->asUser();
if (_st.showLastSeenVisible
&& user
&& !user->isSelf()
&& !user->isBot()
&& !user->isServiceUser()
&& user->session().premiumPossible()) {
if (user->session().premium()) {
if (user->lastseen().isHiddenByMe()) {
user->updateFullForced();
}
_showLastSeen->hide();
return;
}
rpl::combine(
user->session().changes().peerFlagsValue(
user,
Data::PeerUpdate::Flag::OnlineStatus),
Data::AmPremiumValue(&user->session())
) | rpl::on_next([=](auto, bool premium) {
const auto wasShown = !_showLastSeen->isHidden();
const auto hiddenByMe = user->lastseen().isHiddenByMe();
const auto shown = hiddenByMe
&& !user->lastseen().isOnline(base::unixtime::now())
&& !premium
&& user->session().premiumPossible();
_showLastSeen->setVisible(shown);
if (wasShown && premium && hiddenByMe) {
user->updateFullForced();
}
}, _showLastSeen->lifetime());
_controller->session().api().userPrivacy().value(
Api::UserPrivacy::Key::LastSeen
) | rpl::filter([=](Api::UserPrivacy::Rule rule) {
return (rule.option == Api::UserPrivacy::Option::Everyone);
}) | rpl::on_next([=] {
if (user->lastseen().isHiddenByMe()) {
user->updateFullForced();
}
}, _showLastSeen->lifetime());
} else {
_showLastSeen->hide();
}
using TextTransform = Ui::RoundButton::TextTransform;
_showLastSeen->setTextTransform(TextTransform::NoTransform);
_showLastSeen->setFullRadius(true);
_showLastSeen->setClickedCallback([=] {
const auto type = Ui::ShowOrPremium::LastSeen;
auto box = Box(Ui::ShowOrPremiumBox, type, user->shortName(), [=] {
_controller->session().api().userPrivacy().save(
::Api::UserPrivacy::Key::LastSeen,
{});
}, [=] {
::Settings::ShowPremium(_controller, u"lastseen_hidden"_q);
});
_controller->show(std::move(box));
});
}
void Cover::setupChildGeometry() {
widthValue(
) | rpl::on_next([this](int newWidth) {
if (_userpic) {
_userpic->moveToLeft(_st.photoLeft, _st.photoTop, newWidth);
} else {
_iconButton->moveToLeft(_st.photoLeft, _st.photoTop, newWidth);
}
if (_changePersonal) {
_changePersonal->moveToLeft(
(_st.photoLeft
+ _st.photo.photoSize
- _changePersonal->width()
+ st::infoEditContactPersonalLeft),
(_userpic->y()
+ _userpic->height()
- _changePersonal->height()));
}
refreshNameGeometry(newWidth);
refreshStatusGeometry(newWidth);
}, lifetime());
}
Cover *Cover::setOnlineCount(rpl::producer<int> &&count) {
std::move(count) | rpl::on_next([=](int value) {
if (_statusLabel) {
_statusLabel->setOnlineCount(value);
refreshStatusGeometry(width());
}
}, lifetime());
return this;
}
std::optional<QImage> Cover::updatedPersonalPhoto() const {
return _personalChosen;
}
void Cover::initViewers(rpl::producer<QString> title) {
using Flag = Data::PeerUpdate::Flag;
std::move(
title
) | rpl::on_next([=](const QString &title) {
_name->setText(title);
refreshNameGeometry(width());
}, lifetime());
_statusLabel->setMembersLinkCallback([=] {
_showSection.fire(Section::Type::Members);
});
_peer->session().changes().peerFlagsValue(
_peer,
Flag::OnlineStatus | Flag::Members
) | rpl::on_next([=] {
_statusLabel->refresh();
refreshStatusGeometry(width());
}, lifetime());
_peer->session().changes().peerFlagsValue(
_peer,
(_peer->isUser() ? Flag::IsContact : Flag::Rights)
) | rpl::on_next([=] {
refreshUploadPhotoOverlay();
}, lifetime());
setupChangePersonal();
}
void Cover::refreshUploadPhotoOverlay() {
if (!_userpic) {
return;
} else if (_role == Role::EditContact) {
_userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
return;
}
const auto canChange = [&] {
if (const auto chat = _peer->asChat()) {
return chat->canEditInformation();
} else if (const auto channel = _peer->asChannel()) {
return channel->canEditInformation()
&& !channel->isMonoforum();
} else if (const auto user = _peer->asUser()) {
return user->isSelf()
|| (user->isContact()
&& !user->isInaccessible()
&& !user->isServiceUser());
}
Unexpected("Peer type in Info::Profile::Cover.");
}();
_userpic->switchChangePhotoOverlay(canChange, [=](
Ui::UserpicButton::ChosenImage chosen) {
using ChosenType = Ui::UserpicButton::ChosenType;
auto result = Api::PeerPhoto::UserPhoto{
base::take<QImage>(chosen.image), // Strange MSVC bug with take.
chosen.markup.documentId,
chosen.markup.colors,
};
switch (chosen.type) {
case ChosenType::Set:
_userpic->showCustom(base::duplicate(result.image));
_peer->session().api().peerPhoto().upload(
_peer,
std::move(result));
break;
case ChosenType::Suggest:
_peer->session().api().peerPhoto().suggest(
_peer,
std::move(result));
break;
}
});
const auto canReport = [=, peer = _peer] {
if (!peer->hasUserpic()) {
return false;
}
const auto user = peer->asUser();
if (!user) {
if (canChange) {
return false;
}
} else if (user->hasPersonalPhoto()
|| user->isSelf()
|| user->isInaccessible()
|| user->isRepliesChat()
|| user->isVerifyCodes()
|| (user->botInfo && user->botInfo->canEditInformation)
|| user->isServiceUser()) {
return false;
}
return true;
};
const auto contextMenu = _userpic->lifetime()
.make_state<base::unique_qptr<Ui::PopupMenu>>();
const auto showMenu = [=, peer = _peer, controller = _controller](
not_null<Ui::RpWidget*> parent) {
if (!canReport()) {
return false;
}
*contextMenu = base::make_unique_q<Ui::PopupMenu>(
parent,
st::popupMenuWithIcons);
contextMenu->get()->addAction(tr::lng_profile_report(tr::now), [=] {
controller->show(
ReportProfilePhotoBox(
peer,
peer->owner().photo(peer->userpicPhotoId())),
Ui::LayerOption::CloseOther);
}, &st::menuIconReport);
contextMenu->get()->popup(QCursor::pos());
return true;
};
base::install_event_filter(_userpic, [showMenu, raw = _userpic.data()](
not_null<QEvent*> e) {
return (e->type() == QEvent::ContextMenu && showMenu(raw))
? base::EventFilterResult::Cancel
: base::EventFilterResult::Continue;
});
if (const auto user = _peer->asUser()) {
_userpic->resetPersonalRequests(
) | rpl::on_next([=] {
user->session().api().peerPhoto().clearPersonal(user);
_userpic->showSource(Ui::UserpicButton::Source::PeerPhoto);
}, lifetime());
}
}
void Cover::setupChangePersonal() {
if (!_changePersonal) {
return;
}
_changePersonal->chosenImages(
) | rpl::on_next([=](Ui::UserpicButton::ChosenImage &&chosen) {
if (chosen.type == Ui::UserpicButton::ChosenType::Suggest) {
_peer->session().api().peerPhoto().suggest(
_peer,
{
std::move(chosen.image),
chosen.markup.documentId,
chosen.markup.colors,
});
} else {
_personalChosen = std::move(chosen.image);
_userpic->showCustom(base::duplicate(*_personalChosen));
_changePersonal->overrideHasPersonalPhoto(true);
_changePersonal->showSource(
Ui::UserpicButton::Source::NonPersonalIfHasPersonal);
}
}, _changePersonal->lifetime());
_changePersonal->resetPersonalRequests(
) | rpl::on_next([=] {
_personalChosen = QImage();
_userpic->showSource(
Ui::UserpicButton::Source::NonPersonalPhoto);
_changePersonal->overrideHasPersonalPhoto(false);
_changePersonal->showCustom(QImage());
}, _changePersonal->lifetime());
}
Cover::~Cover() {
base::take(_badgeTooltip);
base::take(_badgeOldTooltips);
}
void Cover::refreshNameGeometry(int newWidth) {
auto nameWidth = newWidth - _st.nameLeft - _st.rightSkip;
const auto verifiedWidget = _verified ? _verified->widget() : nullptr;
const auto badgeWidget = _badge ? _badge->widget() : nullptr;
if (verifiedWidget) {
nameWidth -= verifiedWidget->width();
}
if (badgeWidget) {
nameWidth -= badgeWidget->width();
}
if (verifiedWidget || badgeWidget) {
nameWidth -= st::infoVerifiedCheckPosition.x();
}
auto nameLeft = _st.nameLeft;
const auto badgeTop = _st.nameTop;
const auto badgeBottom = _st.nameTop + _name->height();
const auto margins = LargeCustomEmojiMargins();
if (_botVerify) {
_botVerify->move(nameLeft - margins.left(), badgeTop, badgeBottom);
if (const auto widget = _botVerify->widget()) {
const auto skip = widget->width()
+ st::infoVerifiedCheckPosition.x();
nameLeft += skip;
nameWidth -= skip;
}
}
_name->resizeToNaturalWidth(nameWidth);
_name->moveToLeft(nameLeft, _st.nameTop, newWidth);
const auto badgeLeft = nameLeft + _name->width();
if (_badge) {
_badge->move(badgeLeft, badgeTop, badgeBottom);
}
if (_verified) {
_verified->move(
badgeLeft + (badgeWidget ? badgeWidget->width() : 0),
badgeTop,
badgeBottom);
}
}
void Cover::refreshStatusGeometry(int newWidth) {
if (const auto rating = _starsRating.get()) {
rating->moveTo(_st.starsRatingLeft, _st.starsRatingTop);
}
const auto statusLeft = _st.statusLeft + _statusShift.current();
auto statusWidth = newWidth - statusLeft - _st.rightSkip;
_status->resizeToNaturalWidth(statusWidth);
_status->moveToLeft(statusLeft, _st.statusTop, newWidth);
const auto left = statusLeft + _status->textMaxWidth();
_showLastSeen->moveToLeft(
left + _st.showLastSeenPosition.x(),
_st.showLastSeenPosition.y(),
newWidth);
}
void Cover::hideBadgeTooltip() {
_badgeTooltipHide.cancel();
if (auto old = base::take(_badgeTooltip)) {
const auto raw = old.get();
_badgeOldTooltips.push_back(std::move(old));
raw->fade(false);
raw->shownValue(
) | rpl::filter(
!rpl::mappers::_1
) | rpl::on_next([=] {
const auto i = ranges::find(
_badgeOldTooltips,
raw,
&std::unique_ptr<BadgeTooltip>::get);
if (i != end(_badgeOldTooltips)) {
_badgeOldTooltips.erase(i);
}
}, raw->lifetime());
}
}
void Cover::setupUniqueBadgeTooltip() {
if (!_badge) {
return;
}
base::timer_once(kWaitBeforeGiftBadge) | rpl::then(
_badge->updated()
) | rpl::on_next([=] {
const auto widget = _badge->widget();
const auto &content = _badgeContent.current();
const auto &collectible = content.emojiStatusId.collectible;
const auto premium = (content.badge == BadgeType::Premium);
const auto id = (collectible && widget && premium)
? collectible->id
: uint64();
if (_badgeCollectibleId == id) {
return;
}
hideBadgeTooltip();
if (!collectible) {
return;
}
const auto parent = _parentForTooltip
? _parentForTooltip()
: _controller->window().widget()->bodyWidget();
_badgeTooltip = std::make_unique<BadgeTooltip>(
parent,
collectible,
widget);
const auto raw = _badgeTooltip.get();
raw->fade(true);
_badgeTooltipHide.callOnce(kGiftBadgeGlares * raw->glarePeriod()
- st::infoGiftTooltip.duration * 1.5);
}, lifetime());
if (const auto raw = _badgeTooltip.get()) {
raw->finishAnimating();
}
}
} // namespace Info::Profile

View File

@@ -0,0 +1,187 @@
/*
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 "info/profile/info_profile_badge.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/abstract_button.h"
#include "base/timer.h"
namespace Window {
class SessionController;
} // namespace Window
namespace Ui {
class UserpicButton;
class FlatLabel;
template <typename Widget>
class SlideWrap;
class RoundButton;
class StarsRating;
} // namespace Ui
namespace HistoryView {
class StickerPlayer;
} // namespace HistoryView
namespace Data {
class ForumTopic;
} // namespace Data
namespace Info {
class Controller;
class Section;
} // namespace Info
namespace style {
struct InfoProfileCover;
} // namespace style
namespace Info::Profile {
class BadgeTooltip;
class EmojiStatusPanel;
class Badge;
class StatusLabel;
[[nodiscard]] QMargins LargeCustomEmojiMargins();
class TopicIconView final {
public:
TopicIconView(
not_null<Data::ForumTopic*> topic,
Fn<bool()> paused,
Fn<void()> update);
TopicIconView(
not_null<Data::ForumTopic*> topic,
Fn<bool()> paused,
Fn<void()> update,
const style::color &generalIconFg);
void paintInRect(QPainter &p, QRect rect);
private:
using StickerPlayer = HistoryView::StickerPlayer;
void setup(not_null<Data::ForumTopic*> topic);
void setupPlayer(not_null<Data::ForumTopic*> topic);
void setupImage(not_null<Data::ForumTopic*> topic);
const not_null<Data::ForumTopic*> _topic;
const style::color &_generalIconFg;
Fn<bool()> _paused;
Fn<void()> _update;
std::shared_ptr<StickerPlayer> _player;
bool _playerUsesTextColor = false;
QImage _image;
rpl::lifetime _lifetime;
};
class TopicIconButton final : public Ui::AbstractButton {
public:
TopicIconButton(
QWidget *parent,
not_null<Window::SessionController*> controller,
not_null<Data::ForumTopic*> topic);
TopicIconButton(
QWidget *parent,
not_null<Data::ForumTopic*> topic,
Fn<bool()> paused);
private:
TopicIconView _view;
};
class Cover final : public Ui::FixedHeightWidget {
public:
enum class Role {
Info,
EditContact,
};
Cover(
QWidget *parent,
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer,
Fn<not_null<QWidget*>()> parentForTooltip = nullptr);
Cover(
QWidget *parent,
not_null<Window::SessionController*> controller,
not_null<Data::ForumTopic*> topic);
Cover(
QWidget *parent,
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer,
Role role,
rpl::producer<QString> title);
~Cover();
Cover *setOnlineCount(rpl::producer<int> &&count);
[[nodiscard]] rpl::producer<Section> showSection() const {
return _showSection.events();
}
[[nodiscard]] std::optional<QImage> updatedPersonalPhoto() const;
private:
Cover(
QWidget *parent,
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer,
Data::ForumTopic *topic,
Role role,
rpl::producer<QString> title,
Fn<not_null<QWidget*>()> parentForTooltip);
void setupShowLastSeen();
void setupChildGeometry();
void initViewers(rpl::producer<QString> title);
void refreshNameGeometry(int newWidth);
void refreshStatusGeometry(int newWidth);
void refreshUploadPhotoOverlay();
void setupUniqueBadgeTooltip();
void setupChangePersonal();
void hideBadgeTooltip();
const style::InfoProfileCover &_st;
const Role _role = Role::Info;
const not_null<Window::SessionController*> _controller;
const not_null<PeerData*> _peer;
const std::unique_ptr<EmojiStatusPanel> _emojiStatusPanel;
const std::unique_ptr<Badge> _botVerify;
rpl::variable<Badge::Content> _badgeContent;
const std::unique_ptr<Badge> _badge;
const std::unique_ptr<Badge> _verified;
const Fn<not_null<QWidget*>()> _parentForTooltip;
std::unique_ptr<BadgeTooltip> _badgeTooltip;
std::vector<std::unique_ptr<BadgeTooltip>> _badgeOldTooltips;
base::Timer _badgeTooltipHide;
uint64 _badgeCollectibleId = 0;
const object_ptr<Ui::UserpicButton> _userpic;
Ui::UserpicButton *_changePersonal = nullptr;
std::optional<QImage> _personalChosen;
object_ptr<TopicIconButton> _iconButton;
object_ptr<Ui::FlatLabel> _name = { nullptr };
std::unique_ptr<Ui::StarsRating> _starsRating;
object_ptr<Ui::FlatLabel> _status = { nullptr };
std::unique_ptr<StatusLabel> _statusLabel;
rpl::variable<int> _statusShift = 0;
object_ptr<Ui::RoundButton> _showLastSeen = { nullptr };
//object_ptr<CoverDropArea> _dropArea = { nullptr };
rpl::event_stream<Section> _showSection;
};
} // namespace Info::Profile

View File

@@ -0,0 +1,344 @@
/*
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 "info/profile/info_profile_emoji_status_panel.h"
#include "api/api_peer_photo.h"
#include "apiwrap.h"
#include "data/data_user.h"
#include "data/data_session.h"
#include "data/data_document.h"
#include "data/data_emoji_statuses.h"
#include "lang/lang_keys.h"
#include "menu/menu_send.h" // SendMenu::Type.
#include "ui/boxes/confirm_box.h"
#include "ui/boxes/time_picker_box.h"
#include "ui/effects/emoji_fly_animation.h"
#include "ui/text/format_values.h"
#include "ui/ui_utility.h"
#include "base/unixtime.h"
#include "boxes/premium_preview_box.h"
#include "window/window_session_controller.h"
#include "window/window_controller.h"
#include "main/main_session.h"
#include "mainwindow.h"
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "styles/style_chat_helpers.h"
namespace Info::Profile {
namespace {
constexpr auto kLimitFirstRow = 8;
void PickUntilBox(not_null<Ui::GenericBox*> box, Fn<void(TimeId)> callback) {
box->setTitle(tr::lng_emoji_status_for_title());
const auto seconds = Ui::DefaultTimePickerValues();
const auto phrases = ranges::views::all(
seconds
) | ranges::views::transform(Ui::FormatMuteFor) | ranges::to_vector;
const auto pickerCallback = Ui::TimePickerBox(box, seconds, phrases, 0);
Ui::ConfirmBox(box, {
.confirmed = [=] {
callback(pickerCallback());
box->closeBox();
},
.confirmText = tr::lng_emoji_status_for_submit(),
.cancelText = tr::lng_cancel(),
});
}
} // namespace
EmojiStatusPanel::EmojiStatusPanel() = default;
EmojiStatusPanel::~EmojiStatusPanel() {
if (hasFocus()) {
// Panel will try to return focus to the layer widget, the problem is
// we are destroying the layer widget probably right now and focusing
// it will lead to a crash, because it destroys its children (how we
// got here) after it clears focus out of itself. So if you return
// the focus inside a child destructor, it won't be cleared at all.
_panel->window()->setFocus();
}
}
void EmojiStatusPanel::setChooseFilter(Fn<bool(EmojiStatusId)> filter) {
_chooseFilter = std::move(filter);
}
void EmojiStatusPanel::show(
not_null<Window::SessionController*> controller,
not_null<QWidget*> button,
Data::CustomEmojiSizeTag animationSizeTag) {
show({
.controller = controller,
.button = button,
.animationSizeTag = animationSizeTag,
.ensureAddedEmojiId = controller->session().user()->emojiStatusId(),
.withCollectibles = true,
});
}
void EmojiStatusPanel::show(Descriptor &&descriptor) {
const auto controller = descriptor.controller;
if (!_panel) {
create(descriptor);
_panel->shownValue(
) | rpl::filter([=] {
return (_panelButton != nullptr);
}) | rpl::on_next([=](bool shown) {
if (shown) {
_panelButton->installEventFilter(_panel.get());
} else {
_panelButton->removeEventFilter(_panel.get());
}
}, _panel->lifetime());
}
const auto button = descriptor.button;
if (const auto previous = _panelButton.data()) {
if (previous != button) {
previous->removeEventFilter(_panel.get());
}
}
_panelButton = button;
_animationSizeTag = descriptor.animationSizeTag;
const auto feed = [=, now = descriptor.ensureAddedEmojiId](
std::vector<EmojiStatusId> list) {
list.insert(begin(list), EmojiStatusId());
if (now && !ranges::contains(list, now)) {
list.push_back(now);
}
_panel->selector()->provideRecentEmoji(list);
};
if (descriptor.backgroundEmojiMode) {
controller->session().api().peerPhoto().emojiListValue(
Api::PeerPhoto::EmojiListType::Background
) | rpl::on_next([=](std::vector<DocumentId> &&list) {
auto tmp = std::vector<EmojiStatusId>();
for (const auto &id : list) {
tmp.push_back(EmojiStatusId{ .documentId = id });
}
feed(std::move(tmp));
}, _panel->lifetime());
} else if (descriptor.channelStatusMode) {
const auto &statuses = controller->session().data().emojiStatuses();
const auto &other = statuses.list(Data::EmojiStatuses::Type::ChannelDefault);
auto list = statuses.list(Data::EmojiStatuses::Type::ChannelColored);
if (list.size() > kLimitFirstRow - 1) {
list.erase(begin(list) + kLimitFirstRow - 1, end(list));
}
list.reserve(list.size() + other.size() + 1);
for (const auto &id : other) {
if (!ranges::contains(list, id)) {
list.push_back(id);
}
}
feed(std::move(list));
} else {
const auto &statuses = controller->session().data().emojiStatuses();
const auto &recent = statuses.list(Data::EmojiStatuses::Type::Recent);
const auto &other = statuses.list(Data::EmojiStatuses::Type::Default);
auto list = statuses.list(Data::EmojiStatuses::Type::Colored);
if (list.size() > kLimitFirstRow - 1) {
list.erase(begin(list) + kLimitFirstRow - 1, end(list));
}
list.reserve(list.size() + recent.size() + other.size() + 1);
for (const auto &id : ranges::views::concat(recent, other)) {
if (!ranges::contains(list, id)) {
list.push_back(id);
}
}
feed(std::move(list));
}
const auto parent = _panel->parentWidget();
const auto global = button->mapToGlobal(QPoint());
const auto local = parent->mapFromGlobal(global);
if (descriptor.backgroundEmojiMode || descriptor.channelStatusMode) {
_panel->moveBottomRight(
local.y() + (st::normalFont->height / 2),
local.x() + button->width() * 3);
} else {
_panel->moveTopRight(
local.y() + button->height() - (st::normalFont->height / 2),
local.x() + button->width() * 3);
}
_panel->toggleAnimated();
}
bool EmojiStatusPanel::hasFocus() const {
return _panel && Ui::InFocusChain(_panel.get());
}
void EmojiStatusPanel::repaint() {
_panel->selector()->update();
}
bool EmojiStatusPanel::paintBadgeFrame(not_null<Ui::RpWidget*> widget) {
if (!_animation) {
return false;
} else if (_animation->paintBadgeFrame(widget)) {
return true;
}
InvokeQueued(_animation->layer(), [=] { _animation = nullptr; });
return false;
}
void EmojiStatusPanel::create(const Descriptor &descriptor) {
using Selector = ChatHelpers::TabbedSelector;
using Descriptor = ChatHelpers::TabbedSelectorDescriptor;
using Mode = ChatHelpers::TabbedSelector::Mode;
const auto controller = descriptor.controller;
const auto body = controller->window().widget()->bodyWidget();
auto features = ChatHelpers::ComposeFeatures();
features.collectibleStatus = descriptor.withCollectibles;
_panel = base::make_unique_q<ChatHelpers::TabbedPanel>(
body,
controller,
object_ptr<Selector>(
nullptr,
Descriptor{
.show = controller->uiShow(),
.st = ((descriptor.backgroundEmojiMode
|| descriptor.channelStatusMode)
? st::backgroundEmojiPan
: st::statusEmojiPan),
.level = Window::GifPauseReason::Layer,
.mode = (descriptor.backgroundEmojiMode
? Mode::BackgroundEmoji
: descriptor.channelStatusMode
? Mode::ChannelStatus
: Mode::EmojiStatus),
.customTextColor = descriptor.customTextColor,
.features = features,
}));
_customTextColor = descriptor.customTextColor;
_backgroundEmojiMode = descriptor.backgroundEmojiMode;
_channelStatusMode = descriptor.channelStatusMode;
_panel->setDropDown(!_backgroundEmojiMode && !_channelStatusMode);
_panel->setDesiredHeightValues(
1.,
st::emojiPanMinHeight / 2,
st::emojiPanMinHeight);
_panel->hide();
struct Chosen {
EmojiStatusId id;
TimeId until = 0;
Ui::MessageSendingAnimationFrom animation;
};
_panel->selector()->contextMenuRequested(
) | rpl::on_next([=] {
_panel->selector()->showMenuWithDetails({});
}, _panel->lifetime());
auto statusChosen = _panel->selector()->customEmojiChosen(
) | rpl::map([=](ChatHelpers::FileChosen data) {
return Chosen{
.id = {
data.collectible ? DocumentId() : data.document->id,
data.collectible,
},
.until = data.options.scheduled,
.animation = data.messageSendingFrom,
};
});
auto emojiChosen = _panel->selector()->emojiChosen(
) | rpl::map([=](ChatHelpers::EmojiChosen data) {
return Chosen{ .animation = data.messageSendingFrom };
});
if (descriptor.backgroundEmojiMode || descriptor.channelStatusMode) {
rpl::merge(
std::move(statusChosen),
std::move(emojiChosen)
) | rpl::on_next([=](const Chosen &chosen) {
const auto owner = &controller->session().data();
startAnimation(owner, body, chosen.id, chosen.animation);
_someCustomChosen.fire({ chosen.id, chosen.until });
_panel->hideAnimated();
}, _panel->lifetime());
} else {
const auto weak = base::make_weak(_panel.get());
const auto accept = [=](Chosen chosen) {
Expects(chosen.until != Selector::kPickCustomTimeId);
// PickUntilBox calls this after EmojiStatusPanel is destroyed!
const auto owner = &controller->session().data();
if (weak) {
startAnimation(owner, body, chosen.id, chosen.animation);
}
owner->emojiStatuses().set(chosen.id, chosen.until);
};
rpl::merge(
std::move(statusChosen),
std::move(emojiChosen)
) | rpl::filter([=](const Chosen &chosen) {
return filter(controller, chosen.id);
}) | rpl::on_next([=](const Chosen &chosen) {
if (chosen.until == Selector::kPickCustomTimeId) {
_panel->hideAnimated();
controller->show(Box(PickUntilBox, [=](TimeId seconds) {
accept({ chosen.id, base::unixtime::now() + seconds });
}));
} else {
accept(chosen);
_panel->hideAnimated();
}
}, _panel->lifetime());
}
}
bool EmojiStatusPanel::filter(
not_null<Window::SessionController*> controller,
EmojiStatusId chosenId) const {
if (_chooseFilter) {
return _chooseFilter(chosenId);
} else if (chosenId && !controller->session().premium()) {
ShowPremiumPreviewBox(controller, PremiumFeature::EmojiStatus);
return false;
}
return true;
}
void EmojiStatusPanel::startAnimation(
not_null<Data::Session*> owner,
not_null<Ui::RpWidget*> body,
EmojiStatusId statusId,
Ui::MessageSendingAnimationFrom from) {
if (!_panelButton || !statusId) {
return;
}
const auto documentId = statusId.collectible
? statusId.collectible->documentId
: statusId.documentId;
auto args = Ui::ReactionFlyAnimationArgs{
.id = { { documentId } },
.flyIcon = from.frame,
.flyFrom = body->mapFromGlobal(from.globalStartGeometry),
.forceFirstFrame = _backgroundEmojiMode,
};
const auto color = _customTextColor
? _customTextColor
: [] { return st::profileVerifiedCheckBg->c; };
_animation = std::make_unique<Ui::EmojiFlyAnimation>(
body,
&owner->reactions(),
std::move(args),
[=] { _animation->repaint(); },
_customTextColor,
_animationSizeTag);
}
} // namespace Info::Profile

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 "base/unique_qptr.h"
namespace Data {
class Session;
enum class CustomEmojiSizeTag : uchar;
} // namespace Data
namespace Window {
class SessionController;
} // namespace Window
namespace Ui {
struct MessageSendingAnimationFrom;
class EmojiFlyAnimation;
class RpWidget;
} // namespace Ui
namespace Ui::Text {
class CustomEmoji;
struct CustomEmojiPaintContext;
} // namespace Ui::Text
namespace ChatHelpers {
class TabbedPanel;
} // namespace ChatHelpers
namespace Info::Profile {
class EmojiStatusPanel final {
public:
EmojiStatusPanel();
~EmojiStatusPanel();
void setChooseFilter(Fn<bool(EmojiStatusId)> filter);
void show(
not_null<Window::SessionController*> controller,
not_null<QWidget*> button,
Data::CustomEmojiSizeTag animationSizeTag = {});
[[nodiscard]] bool hasFocus() const;
struct Descriptor {
not_null<Window::SessionController*> controller;
not_null<QWidget*> button;
Data::CustomEmojiSizeTag animationSizeTag = {};
EmojiStatusId ensureAddedEmojiId;
Fn<QColor()> customTextColor;
bool backgroundEmojiMode = false;
bool channelStatusMode = false;
bool withCollectibles = false;
};
void show(Descriptor &&descriptor);
void repaint();
struct CustomChosen {
EmojiStatusId id;
TimeId until = 0;
};
[[nodiscard]] rpl::producer<CustomChosen> someCustomChosen() const {
return _someCustomChosen.events();
}
bool paintBadgeFrame(not_null<Ui::RpWidget*> widget);
private:
void create(const Descriptor &descriptor);
[[nodiscard]] bool filter(
not_null<Window::SessionController*> controller,
EmojiStatusId chosenId) const;
void startAnimation(
not_null<Data::Session*> owner,
not_null<Ui::RpWidget*> body,
EmojiStatusId statusId,
Ui::MessageSendingAnimationFrom from);
base::unique_qptr<ChatHelpers::TabbedPanel> _panel;
Fn<QColor()> _customTextColor;
Fn<bool(EmojiStatusId)> _chooseFilter;
QPointer<QWidget> _panelButton;
std::unique_ptr<Ui::EmojiFlyAnimation> _animation;
rpl::event_stream<CustomChosen> _someCustomChosen;
Data::CustomEmojiSizeTag _animationSizeTag = {};
bool _backgroundEmojiMode = false;
bool _channelStatusMode = false;
};
} // namespace Info::Profile

View File

@@ -0,0 +1,40 @@
/*
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 "info/profile/info_profile_icon.h"
namespace Info {
namespace Profile {
FloatingIcon::FloatingIcon(
RpWidget *parent,
const style::icon &icon,
QPoint position)
: FloatingIcon(parent, icon, position, Tag{}) {
}
FloatingIcon::FloatingIcon(
RpWidget *parent,
const style::icon &icon,
QPoint position,
const Tag &)
: RpWidget(parent)
, _icon(&icon)
, _point(position) {
setGeometry(QRect(
QPoint(0, 0),
QSize(_point.x() + _icon->width(), _point.y() + _icon->height())));
setAttribute(Qt::WA_TransparentForMouseEvents);
}
void FloatingIcon::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
_icon->paint(p, _point, width());
}
} // namespace Profile
} // namespace Info

View File

@@ -0,0 +1,40 @@
/*
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 "ui/rp_widget.h"
namespace Info {
namespace Profile {
class FloatingIcon : public Ui::RpWidget {
public:
FloatingIcon(
RpWidget *parent,
const style::icon &icon,
QPoint position);
protected:
void paintEvent(QPaintEvent *e) override;
private:
struct Tag {
};
FloatingIcon(
RpWidget *parent,
const style::icon &icon,
QPoint position,
const Tag &);
not_null<const style::icon*> _icon;
QPoint _point;
};
} // namespace Profile
} // namespace Info

View File

@@ -0,0 +1,471 @@
/*
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 "info/profile/info_profile_inner_widget.h"
#include "info/info_controller.h"
#include "info/info_memento.h"
#include "info/profile/info_profile_widget.h"
#include "info/profile/info_profile_icon.h"
#include "info/profile/info_profile_members.h"
#include "info/profile/info_profile_music_button.h"
#include "info/profile/info_profile_top_bar.h"
#include "info/profile/info_profile_actions.h"
#include "info/media/info_media_buttons.h"
#include "info/saved/info_saved_music_widget.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
#include "data/data_document.h"
#include "data/data_forum_topic.h"
#include "data/data_peer.h"
#include "data/data_photo.h"
#include "data/data_file_origin.h"
#include "data/data_user.h"
#include "data/data_saved_music.h"
#include "data/data_saved_sublist.h"
#include "info/saved/info_saved_music_common.h"
#include "info_profile_actions.h"
#include "main/main_session.h"
#include "apiwrap.h"
#include "api/api_peer_photo.h"
#include "lang/lang_keys.h"
#include "ui/text/format_song_document_name.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/shadow.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/vertical_list.h"
#include "ui/ui_utility.h"
#include "styles/style_info.h"
namespace Info {
namespace Profile {
namespace {
void AddAboutVerification(
not_null<Ui::VerticalLayout*> layout,
not_null<PeerData*> peer) {
const auto inner = layout->add(object_ptr<Ui::VerticalLayout>(layout));
peer->session().changes().peerFlagsValue(
peer,
Data::PeerUpdate::Flag::VerifyInfo
) | rpl::on_next([=] {
const auto info = peer->botVerifyDetails();
while (inner->count()) {
delete inner->widgetAt(0);
}
if (!info) {
Ui::AddDivider(inner);
} else {
auto hasMainApp = false;
if (const auto user = peer->asUser()) {
if (user->botInfo) {
hasMainApp = user->botInfo->hasMainApp;
}
}
if (!hasMainApp && !info->description.empty()) {
Ui::AddDividerText(inner, rpl::single(info->description));
}
}
inner->resizeToWidth(inner->width());
}, inner->lifetime());
}
} // namespace
InnerWidget::InnerWidget(
QWidget *parent,
not_null<Controller*> controller,
Origin origin)
: RpWidget(parent)
, _controller(controller)
, _peer(_controller->key().peer())
, _migrated(_controller->migrated())
, _topic(_controller->key().topic())
, _sublist(_controller->key().sublist())
, _content(setupContent(this, origin)) {
_content->heightValue(
) | rpl::on_next([this](int height) {
if (!_inResize) {
resizeToWidth(width());
updateDesiredHeight();
}
}, lifetime());
}
rpl::producer<> InnerWidget::backRequest() const {
return _backClicks.events();
}
object_ptr<Ui::RpWidget> InnerWidget::setupContent(
not_null<RpWidget*> parent,
Origin origin) {
if (const auto user = _peer->asUser()) {
user->session().changes().peerFlagsValue(
user,
Data::PeerUpdate::Flag::FullInfo
) | rpl::on_next([=] {
auto &photos = user->session().api().peerPhoto();
if (const auto original = photos.nonPersonalPhoto(user)) {
// Preload it for the edit contact box.
_nonPersonalView = original->createMediaView();
const auto id = peerToUser(user->id);
original->load(Data::FileOriginFullUser{ id });
}
}, lifetime());
}
auto result = object_ptr<Ui::VerticalLayout>(parent);
setupSavedMusic(result);
if (_topic && _topic->creating()) {
return result;
}
auto mainTracker = Ui::MultiSlideTracker();
auto sharedTracker = Ui::MultiSlideTracker();
auto dividerOverridden = rpl::variable<bool>(false);
AddDetails(
result,
_controller,
_peer,
_topic,
_sublist,
origin,
mainTracker,
dividerOverridden);
auto showDivider = rpl::combine(
mainTracker.atLeastOneShownValue(),
dividerOverridden.value()
) | rpl::map([](bool main, bool dividerOverridden) {
return dividerOverridden ? false : main;
}) | rpl::distinct_until_changed();
result->add(
setupSharedMedia(
result.data(),
rpl::duplicate(showDivider),
sharedTracker));
if (_topic || _sublist) {
return result;
}
{
auto buttons = SetupChannelMembersAndManage(
_controller,
result.data(),
_peer);
if (buttons) {
result->add(std::move(buttons));
}
}
auto showNext = rpl::combine(
std::move(showDivider),
sharedTracker.atLeastOneShownValue()
) | rpl::map([](bool show, bool shared) {
return show || shared;
}) | rpl::distinct_until_changed();
if (auto actions = SetupActions(_controller, result.data(), _peer)) {
addAboutVerificationOrDivider(result, rpl::duplicate(showNext));
result->add(std::move(actions));
}
if (!_aboutVerificationAdded) {
AddAboutVerification(result, _peer);
}
if (_peer->isChat() || _peer->isMegagroup()) {
if (!_peer->isMonoforum()) {
setupMembers(result.data(), rpl::duplicate(showNext));
}
}
return result;
}
void InnerWidget::setupMembers(
not_null<Ui::VerticalLayout*> container,
rpl::producer<bool> showDivider) {
auto wrap = container->add(object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container)));
const auto inner = wrap->entity();
addAboutVerificationOrDivider(inner, std::move(showDivider));
_members = inner->add(object_ptr<Members>(inner, _controller));
_members->scrollToRequests(
) | rpl::on_next([this](Ui::ScrollToRequest request) {
auto min = (request.ymin < 0)
? request.ymin
: MapFrom(this, _members, QPoint(0, request.ymin)).y();
auto max = (request.ymin < 0)
? MapFrom(this, _members, QPoint()).y()
: (request.ymax < 0)
? request.ymax
: MapFrom(this, _members, QPoint(0, request.ymax)).y();
_scrollToRequests.fire({ min, max });
}, _members->lifetime());
_members->onlineCountValue(
) | rpl::on_next([=](int count) {
_onlineCount.fire_copy(count);
}, _members->lifetime());
using namespace rpl::mappers;
wrap->toggleOn(
_members->fullCountValue() | rpl::map(_1 > 0),
anim::type::instant);
}
void InnerWidget::setupSavedMusic(not_null<Ui::VerticalLayout*> container) {
Info::Saved::SetupSavedMusic(
container,
_controller,
_sublist ? _sublist->sublistPeer() : _peer,
_topBarColor.value());
}
void InnerWidget::addAboutVerificationOrDivider(
not_null<Ui::VerticalLayout*> content,
rpl::producer<bool> showDivider) {
if (rpl::variable<bool>(rpl::duplicate(showDivider)).current()) {
if (_aboutVerificationAdded) {
Ui::AddDivider(content);
} else {
AddAboutVerification(content, _peer);
_aboutVerificationAdded = true;
}
} else {
const auto wrap = content->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
content,
object_ptr<Ui::VerticalLayout>(content)));
Ui::AddDivider(wrap->entity());
wrap->setDuration(
st::infoSlideDuration
)->toggleOn(rpl::duplicate(showDivider));
}
}
object_ptr<Ui::RpWidget> InnerWidget::setupSharedMedia(
not_null<RpWidget*> parent,
rpl::producer<bool> showDivider,
Ui::MultiSlideTracker &sharedTracker) {
using namespace rpl::mappers;
using MediaType = Media::Type;
const auto peer = _sublist ? _sublist->sublistPeer() : _peer;
auto content = object_ptr<Ui::VerticalLayout>(parent);
auto &tracker = sharedTracker;
auto addMediaButton = [&](
MediaType type,
const style::icon &icon) {
auto result = Media::AddButton(
content,
_controller,
peer,
_topic ? _topic->rootId() : MsgId(),
_sublist ? _sublist->sublistPeer()->id : PeerId(),
_migrated,
type,
tracker);
object_ptr<Profile::FloatingIcon>(
result,
icon,
st::infoSharedMediaButtonIconPosition);
};
auto addCommonGroupsButton = [&](
not_null<UserData*> user,
const style::icon &icon) {
auto result = Media::AddCommonGroupsButton(
content,
_controller,
user,
tracker);
object_ptr<Profile::FloatingIcon>(
result,
icon,
st::infoSharedMediaButtonIconPosition);
};
const auto addSimilarPeersButton = [&](
not_null<PeerData*> peer,
const style::icon &icon) {
auto result = Media::AddSimilarPeersButton(
content,
_controller,
peer,
tracker);
object_ptr<Profile::FloatingIcon>(
result,
icon,
st::infoSharedMediaButtonIconPosition);
};
auto addStoriesButton = [&](
not_null<PeerData*> peer,
const style::icon &icon) {
if (peer->isChat()) {
return;
}
auto result = Media::AddStoriesButton(
content,
_controller,
peer,
tracker);
object_ptr<Profile::FloatingIcon>(
result,
icon,
st::infoSharedMediaButtonIconPosition);
};
auto addSavedSublistButton = [&](
not_null<PeerData*> peer,
const style::icon &icon) {
auto result = Media::AddSavedSublistButton(
content,
_controller,
peer,
tracker);
object_ptr<Profile::FloatingIcon>(
result,
icon,
st::infoSharedMediaButtonIconPosition);
};
auto addPeerGiftsButton = [&](
not_null<PeerData*> peer,
const style::icon &icon) {
auto result = Media::AddPeerGiftsButton(
content,
_controller,
peer,
tracker);
object_ptr<Profile::FloatingIcon>(
result,
icon,
st::infoSharedMediaButtonIconPosition);
};
if (!_topic) {
addStoriesButton(peer, st::infoIconMediaStories);
addPeerGiftsButton(peer, st::infoIconMediaGifts);
addSavedSublistButton(peer, st::infoIconMediaSaved);
}
addMediaButton(MediaType::Photo, st::infoIconMediaPhoto);
addMediaButton(MediaType::Video, st::infoIconMediaVideo);
addMediaButton(MediaType::File, st::infoIconMediaFile);
addMediaButton(MediaType::MusicFile, st::infoIconMediaAudio);
addMediaButton(MediaType::Link, st::infoIconMediaLink);
addMediaButton(MediaType::RoundVoiceFile, st::infoIconMediaVoice);
addMediaButton(MediaType::GIF, st::infoIconMediaGif);
if (const auto bot = peer->asBot()) {
addCommonGroupsButton(bot, st::infoIconMediaGroup);
addSimilarPeersButton(bot, st::infoIconMediaBot);
} else if (const auto channel = peer->asBroadcast()) {
addSimilarPeersButton(channel, st::infoIconMediaChannel);
} else if (const auto user = peer->asUser()) {
addCommonGroupsButton(user, st::infoIconMediaGroup);
}
auto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
parent,
object_ptr<Ui::VerticalLayout>(parent)
);
result->setDuration(
st::infoSlideDuration
)->toggleOn(
tracker.atLeastOneShownValue()
);
auto layout = result->entity();
addAboutVerificationOrDivider(layout, std::move(showDivider));
Ui::AddSkip(layout, st::infoSharedMediaBottomSkip);
layout->add(std::move(content));
Ui::AddSkip(layout, st::infoSharedMediaBottomSkip);
_sharedMediaWrap = result;
return result;
}
int InnerWidget::countDesiredHeight() const {
return _content->height() + (_members
? (_members->desiredHeight() - _members->height())
: 0);
}
void InnerWidget::visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) {
setChildVisibleTopBottom(_content, visibleTop, visibleBottom);
}
void InnerWidget::saveState(not_null<Memento*> memento) {
if (_members) {
memento->setMembersState(_members->saveState());
}
}
void InnerWidget::restoreState(not_null<Memento*> memento) {
if (_members) {
_members->restoreState(memento->membersState());
}
if (_sharedMediaWrap) {
_sharedMediaWrap->finishAnimating();
}
}
rpl::producer<Ui::ScrollToRequest> InnerWidget::scrollToRequests() const {
return _scrollToRequests.events();
}
rpl::producer<int> InnerWidget::desiredHeightValue() const {
return _desiredHeight.events_starting_with(countDesiredHeight());
}
int InnerWidget::resizeGetHeight(int newWidth) {
_inResize = true;
auto guard = gsl::finally([&] { _inResize = false; });
_content->resizeToWidth(newWidth);
_content->moveToLeft(0, 0);
updateDesiredHeight();
return _content->heightNoMargins();
}
void InnerWidget::enableBackButton() {
_backToggles.force_assign(true);
}
void InnerWidget::showFinished() {
_showFinished.fire({});
}
bool InnerWidget::hasFlexibleTopBar() const {
return true;
}
base::weak_qptr<Ui::RpWidget> InnerWidget::createPinnedToTop(
not_null<Ui::RpWidget*> parent) {
const auto content = Ui::CreateChild<TopBar>(
parent,
TopBar::Descriptor{
.controller = _controller->parentController(),
.key = _controller->key(),
.wrap = _controller->wrapValue(),
.peer = _sublist ? _sublist->sublistPeer().get() : nullptr,
.backToggles = _backToggles.value(),
.showFinished = _showFinished.events(),
});
content->backRequest(
) | rpl::start_to_stream(_backClicks, content->lifetime());
content->setOnlineCount(_onlineCount.events());
_topBarColor = content->edgeColor();
return base::make_weak(not_null<Ui::RpWidget*>{ content });
}
base::weak_qptr<Ui::RpWidget> InnerWidget::createPinnedToBottom(
not_null<Ui::RpWidget*> parent) {
return nullptr;
}
} // namespace Profile
} // namespace Info

View File

@@ -0,0 +1,124 @@
/*
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 "ui/rp_widget.h"
#include "base/object_ptr.h"
namespace Data {
class ForumTopic;
class SavedSublist;
class PhotoMedia;
} // namespace Data
namespace Window {
class SessionController;
} // namespace Window
namespace Ui {
class VerticalLayout;
template <typename Widget>
class SlideWrap;
struct ScrollToRequest;
class MultiSlideTracker;
} // namespace Ui
namespace Info {
enum class Wrap;
class Controller;
namespace Profile {
class Memento;
class Members;
struct Origin;
class InnerWidget final : public Ui::RpWidget {
public:
InnerWidget(
QWidget *parent,
not_null<Controller*> controller,
Origin origin);
[[nodiscard]] rpl::producer<> backRequest() const;
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
rpl::producer<Ui::ScrollToRequest> scrollToRequests() const;
rpl::producer<int> desiredHeightValue() const override;
bool hasFlexibleTopBar() const;
base::weak_qptr<Ui::RpWidget> createPinnedToTop(
not_null<Ui::RpWidget*> parent);
base::weak_qptr<Ui::RpWidget> createPinnedToBottom(
not_null<Ui::RpWidget*> parent);
void enableBackButton();
void showFinished();
protected:
int resizeGetHeight(int newWidth) override;
void visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) override;
private:
object_ptr<RpWidget> setupContent(
not_null<RpWidget*> parent,
Origin origin);
object_ptr<RpWidget> setupSharedMedia(
not_null<RpWidget*> parent,
rpl::producer<bool> showDivider,
Ui::MultiSlideTracker &sharedTracker);
void setupMembers(
not_null<Ui::VerticalLayout*> container,
rpl::producer<bool> showDivider);
void setupSavedMusic(not_null<Ui::VerticalLayout*> container);
int countDesiredHeight() const;
void updateDesiredHeight() {
_desiredHeight.fire(countDesiredHeight());
}
void addAboutVerificationOrDivider(
not_null<Ui::VerticalLayout*> content,
rpl::producer<bool> showDivider);
const not_null<Controller*> _controller;
const not_null<PeerData*> _peer;
PeerData * const _migrated = nullptr;
Data::ForumTopic * const _topic = nullptr;
Data::SavedSublist * const _sublist = nullptr;
bool _inResize = false;
rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
rpl::event_stream<int> _desiredHeight;
rpl::variable<bool> _backToggles;
rpl::event_stream<> _backClicks;
rpl::event_stream<int> _onlineCount;
rpl::event_stream<> _showFinished;
PeerData *_reactionGroup = nullptr;
std::shared_ptr<Data::PhotoMedia> _nonPersonalView;
rpl::variable<std::optional<QColor>> _topBarColor;
Members *_members = nullptr;
Ui::SlideWrap<RpWidget> *_sharedMediaWrap = nullptr;
object_ptr<RpWidget> _content;
bool _aboutVerificationAdded = false;
};
} // namespace Profile
} // namespace Info

View File

@@ -0,0 +1,472 @@
/*
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 "info/profile/info_profile_members.h"
#include <rpl/combine.h>
#include "info/profile/info_profile_widget.h"
#include "info/profile/info_profile_values.h"
#include "info/profile/info_profile_icon.h"
#include "info/profile/info_profile_values.h"
#include "info/profile/info_profile_members_controllers.h"
#include "info/members/info_members_widget.h"
#include "info/info_content_widget.h"
#include "info/info_controller.h"
#include "info/info_memento.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/scroll_area.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/search_field_controller.h"
#include "lang/lang_keys.h"
#include "ui/boxes/confirm_box.h"
#include "boxes/peers/add_participants_box.h"
#include "window/window_session_controller.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_user.h"
#include "styles/style_boxes.h"
#include "styles/style_info.h"
namespace Info {
namespace Profile {
namespace {
constexpr auto kEnableSearchMembersAfterCount = 20;
} // namespace
Members::Members(
QWidget *parent,
not_null<Controller*> controller)
: RpWidget(parent)
, _show(controller->uiShow())
, _controller(controller)
, _peer(_controller->key().peer())
, _listController(CreateMembersController(controller, _peer)) {
_listController->setStoriesShown(true);
setupHeader();
setupList();
setContent(_list.data());
_listController->setDelegate(static_cast<PeerListDelegate*>(this));
_controller->searchFieldController()->queryValue(
) | rpl::on_next([this](QString &&query) {
peerListScrollToTop();
content()->searchQueryChanged(std::move(query));
}, lifetime());
MembersCountValue(
_peer
) | rpl::on_next([this](int count) {
const auto enabled = (count >= kEnableSearchMembersAfterCount);
_controller->setSearchEnabledByContent(enabled);
}, lifetime());
}
int Members::desiredHeight() const {
auto desired = _header ? _header->height() : 0;
auto count = [this] {
if (auto chat = _peer->asChat()) {
return chat->count;
} else if (auto channel = _peer->asChannel()) {
return channel->membersCount();
}
return 0;
}();
desired += qMax(count, _list->fullRowsCount())
* st::infoMembersList.item.height;
return qMax(height(), desired);
}
rpl::producer<int> Members::onlineCountValue() const {
return _listController->onlineCountValue();
}
rpl::producer<int> Members::fullCountValue() const {
return _listController->fullCountValue();
}
rpl::producer<Ui::ScrollToRequest> Members::scrollToRequests() const {
return _scrollToRequests.events();
}
std::unique_ptr<MembersState> Members::saveState() {
auto result = std::make_unique<MembersState>();
result->list = _listController->saveState();
//if (_searchShown) {
// result->search = _searchField->getLastText();
//}
return result;
}
void Members::restoreState(std::unique_ptr<MembersState> state) {
if (!state) {
return;
}
_listController->restoreState(std::move(state->list));
//updateSearchEnabledByContent();
//if (!_controller->searchFieldController()->query().isEmpty()) {
// if (!_searchShown) {
// toggleSearch(anim::type::instant);
// }
//} else if (_searchShown) {
// toggleSearch(anim::type::instant);
//}
}
void Members::setupHeader() {
if (_controller->section().type() == Section::Type::Members) {
return;
}
_header = object_ptr<Ui::FixedHeightWidget>(
this,
st::infoMembersHeader);
auto parent = _header.data();
_openMembers = Ui::CreateChild<Ui::SettingsButton>(
parent,
rpl::single(QString()));
object_ptr<FloatingIcon>(
parent,
st::infoIconMembers,
st::infoGroupMembersIconPosition);
_titleWrap = Ui::CreateChild<Ui::RpWidget>(parent);
_title = setupTitle();
_addMember = Ui::CreateChild<Ui::IconButton>(
_openMembers,
st::infoMembersAddMember);
//_searchField = _controller->searchFieldController()->createField(
// parent,
// st::infoMembersSearchField);
_search = Ui::CreateChild<Ui::IconButton>(
_openMembers,
st::infoMembersSearch);
//_cancelSearch = Ui::CreateChild<Ui::CrossButton>(
// parent,
// st::infoMembersCancelSearch);
setupButtons();
//_controller->wrapValue(
//) | rpl::on_next([this](Wrap wrap) {
// _wrap = wrap;
// updateSearchOverrides();
//}, lifetime());
widthValue(
) | rpl::on_next([this](int width) {
_header->resizeToWidth(width);
}, _header->lifetime());
}
object_ptr<Ui::FlatLabel> Members::setupTitle() {
auto visible = _peer->isMegagroup()
? CanViewParticipantsValue(_peer->asMegagroup())
: rpl::single(true);
auto result = object_ptr<Ui::FlatLabel>(
_titleWrap,
rpl::conditional(
std::move(visible),
tr::lng_chat_status_members(
lt_count_decimal,
MembersCountValue(_peer) | tr::to_count(),
tr::upper),
tr::lng_channel_admins(tr::upper)),
st::infoBlockHeaderLabel);
result->setAttribute(Qt::WA_TransparentForMouseEvents);
return result;
}
void Members::setupButtons() {
using namespace rpl::mappers;
_openMembers->addClickHandler([this] {
showMembersWithSearch(false);
});
//_searchField->hide();
//_cancelSearch->setVisible(false);
auto visible = _peer->isMegagroup()
? CanViewParticipantsValue(_peer->asMegagroup())
: rpl::single(true);
rpl::duplicate(visible) | rpl::on_next([=](bool visible) {
_openMembers->setVisible(visible);
}, lifetime());
auto addMemberShown = CanAddMemberValue(
_peer
) | rpl::start_spawning(lifetime());
_addMember->showOn(rpl::duplicate(addMemberShown));
_addMember->addClickHandler([this] { // TODO throttle(ripple duration)
this->addMember();
});
auto searchShown = MembersCountValue(_peer)
| rpl::map(_1 >= kEnableSearchMembersAfterCount)
| rpl::distinct_until_changed()
| rpl::start_spawning(lifetime());
_search->showOn(rpl::duplicate(searchShown));
_search->addClickHandler([this] { // TODO throttle(ripple duration)
this->showMembersWithSearch(true);
});
//_cancelSearch->addClickHandler([this] {
// this->cancelSearch();
//});
rpl::combine(
std::move(addMemberShown),
std::move(searchShown),
std::move(visible)
) | rpl::on_next([this] {
updateHeaderControlsGeometry(width());
}, lifetime());
}
void Members::setupList() {
auto topSkip = _header ? _header->height() : 0;
_listController->setStyleOverrides(&st::infoMembersList);
_listController->setStoriesShown(true);
_list = object_ptr<ListWidget>(
this,
_listController.get());
_list->scrollToRequests(
) | rpl::on_next([this](Ui::ScrollToRequest request) {
auto addmin = (request.ymin < 0 || !_header)
? 0
: _header->height();
auto addmax = (request.ymax < 0 || !_header)
? 0
: _header->height();
_scrollToRequests.fire({
request.ymin + addmin,
request.ymax + addmax });
}, _list->lifetime());
widthValue(
) | rpl::on_next([this](int newWidth) {
_list->resizeToWidth(newWidth);
}, _list->lifetime());
_list->heightValue(
) | rpl::on_next([=](int listHeight) {
auto newHeight = (listHeight > st::membersMarginBottom)
? (topSkip
+ listHeight
+ st::membersMarginBottom)
: 0;
resize(width(), newHeight);
}, _list->lifetime());
_list->moveToLeft(0, topSkip);
}
int Members::resizeGetHeight(int newWidth) {
if (_header) {
updateHeaderControlsGeometry(newWidth);
}
return heightNoMargins();
}
//void Members::updateSearchEnabledByContent() {
// _controller->setSearchEnabledByContent(
// peerListFullRowsCount() >= kEnableSearchMembersAfterCount);
//}
void Members::updateHeaderControlsGeometry(int newWidth) {
_openMembers->setGeometry(0, st::infoProfileSkip, newWidth, st::infoMembersButton.height);
auto availableWidth = newWidth
- st::infoMembersButtonPosition.x();
//auto cancelLeft = availableWidth - _cancelSearch->width();
//_cancelSearch->moveToLeft(
// cancelLeft,
// st::infoMembersButtonPosition.y());
//auto searchShownLeft = st::infoIconPosition.x()
// - st::infoMembersSearch.iconPosition.x();
//auto searchHiddenLeft = availableWidth - _search->width();
//auto searchShown = _searchShownAnimation.value(_searchShown ? 1. : 0.);
//auto searchCurrentLeft = anim::interpolate(
// searchHiddenLeft,
// searchShownLeft,
// searchShown);
//_search->moveToLeft(
// searchCurrentLeft,
// st::infoMembersButtonPosition.y());
//if (!_search->isHidden()) {
// availableWidth -= st::infoMembersSearch.width;
//}
_addMember->moveToLeft(
availableWidth - _addMember->width(),
st::infoMembersButtonPosition.y(),
newWidth);
if (!_addMember->isHidden()) {
availableWidth -= st::infoMembersSearch.width;
}
_search->moveToLeft(
availableWidth - _search->width(),
st::infoMembersButtonPosition.y(),
newWidth);
//auto fieldLeft = anim::interpolate(
// cancelLeft,
// st::infoBlockHeaderPosition.x(),
// searchShown);
//_searchField->setGeometryToLeft(
// fieldLeft,
// st::infoMembersSearchTop,
// cancelLeft - fieldLeft,
// _searchField->height());
//_titleWrap->resize(
// searchCurrentLeft - st::infoBlockHeaderPosition.x(),
// _title->height());
_titleWrap->resize(
availableWidth - _addMember->width() - st::infoBlockHeaderPosition.x(),
_title->height());
_titleWrap->moveToLeft(
st::infoBlockHeaderPosition.x(),
st::infoBlockHeaderPosition.y(),
newWidth);
_titleWrap->setAttribute(Qt::WA_TransparentForMouseEvents);
//_title->resizeToWidth(searchHiddenLeft);
_title->resizeToWidth(_titleWrap->width());
_title->moveToLeft(0, 0);
}
void Members::addMember() {
if (const auto chat = _peer->asChat()) {
AddParticipantsBoxController::Start(_controller, chat);
} else if (const auto channel = _peer->asChannel()) {
const auto state = _listController->saveState();
const auto users = ranges::views::all(
state->list
) | ranges::views::transform([](not_null<PeerData*> peer) {
return peer->asUser();
}) | ranges::to_vector;
AddParticipantsBoxController::Start(
_controller,
channel,
{ users.begin(), users.end() });
}
}
void Members::showMembersWithSearch(bool withSearch) {
//if (!_searchShown) {
// toggleSearch();
//}
auto contentMemento = std::make_shared<Info::Members::Memento>(
_controller);
contentMemento->setState(saveState());
contentMemento->setSearchStartsFocused(withSearch);
auto mementoStack = std::vector<std::shared_ptr<ContentMemento>>();
mementoStack.push_back(std::move(contentMemento));
_controller->showSection(
std::make_shared<Info::Memento>(std::move(mementoStack)));
}
//void Members::toggleSearch(anim::type animated) {
// _searchShown = !_searchShown;
// _cancelSearch->toggle(_searchShown, animated);
// if (animated == anim::type::normal) {
// _searchShownAnimation.start(
// [this] { searchAnimationCallback(); },
// _searchShown ? 0. : 1.,
// _searchShown ? 1. : 0.,
// st::slideWrapDuration);
// } else {
// _searchShownAnimation.finish();
// searchAnimationCallback();
// }
// _search->setDisabled(_searchShown);
// if (_searchShown) {
// _searchField->show();
// _searchField->setFocus();
// } else {
// setFocus();
// }
//}
//
//void Members::searchAnimationCallback() {
// if (!_searchShownAnimation.animating()) {
// _searchField->setVisible(_searchShown);
// updateSearchOverrides();
// _search->setPointerCursor(!_searchShown);
// }
// updateHeaderControlsGeometry(width());
//}
//
//void Members::updateSearchOverrides() {
// auto iconOverride = !_searchShown
// ? nullptr
// : (_wrap == Wrap::Layer)
// ? &st::infoMembersSearchActiveLayer
// : &st::infoMembersSearchActive;
// _search->setIconOverride(iconOverride, iconOverride);
//}
//
//void Members::cancelSearch() {
// if (_searchShown) {
// if (!_searchField->getLastText().isEmpty()) {
// _searchField->setText(QString());
// _searchField->setFocus();
// } else {
// toggleSearch();
// }
// }
//}
void Members::visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) {
setChildVisibleTopBottom(_list, visibleTop, visibleBottom);
}
void Members::peerListSetTitle(rpl::producer<QString> title) {
}
void Members::peerListSetAdditionalTitle(rpl::producer<QString> title) {
}
bool Members::peerListIsRowChecked(not_null<PeerListRow*> row) {
return false;
}
int Members::peerListSelectedRowsCount() {
return 0;
}
void Members::peerListScrollToTop() {
_scrollToRequests.fire({ -1, -1 });
}
void Members::peerListAddSelectedPeerInBunch(not_null<PeerData*> peer) {
Unexpected("Item selection in Info::Profile::Members.");
}
void Members::peerListAddSelectedRowInBunch(not_null<PeerListRow*> row) {
Unexpected("Item selection in Info::Profile::Members.");
}
void Members::peerListFinishSelectedRowsBunch() {
}
std::shared_ptr<Main::SessionShow> Members::peerListUiShow() {
return _show;
}
void Members::peerListSetDescription(
object_ptr<Ui::FlatLabel> description) {
description.destroy();
}
} // namespace Profile
} // namespace Info

View File

@@ -0,0 +1,139 @@
/*
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 "ui/rp_widget.h"
#include "boxes/peer_list_box.h"
class ParticipantsBoxController;
namespace Ui {
class InputField;
class CrossButton;
class IconButton;
class FlatLabel;
struct ScrollToRequest;
class AbstractButton;
class SettingsButton;
} // namespace Ui
namespace Window {
class Show;
} // namespace Window
namespace Info {
class Controller;
enum class Wrap;
namespace Profile {
class Memento;
struct MembersState {
std::unique_ptr<PeerListState> list;
std::optional<QString> search;
};
class Members
: public Ui::RpWidget
, private PeerListContentDelegate {
public:
Members(
QWidget *parent,
not_null<Controller*> controller);
rpl::producer<Ui::ScrollToRequest> scrollToRequests() const;
std::unique_ptr<MembersState> saveState();
void restoreState(std::unique_ptr<MembersState> state);
[[nodiscard]] int desiredHeight() const;
[[nodiscard]] rpl::producer<int> onlineCountValue() const;
[[nodiscard]] rpl::producer<int> fullCountValue() const;
protected:
void visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) override;
int resizeGetHeight(int newWidth) override;
private:
using ListWidget = PeerListContent;
// PeerListContentDelegate interface.
void peerListSetTitle(rpl::producer<QString> title) override;
void peerListSetAdditionalTitle(rpl::producer<QString> title) override;
bool peerListIsRowChecked(not_null<PeerListRow*> row) override;
int peerListSelectedRowsCount() override;
void peerListScrollToTop() override;
void peerListAddSelectedPeerInBunch(
not_null<PeerData*> peer) override;
void peerListAddSelectedRowInBunch(
not_null<PeerListRow*> row) override;
void peerListFinishSelectedRowsBunch() override;
void peerListSetDescription(
object_ptr<Ui::FlatLabel> description) override;
std::shared_ptr<Main::SessionShow> peerListUiShow() override;
//void peerListAppendRow(
// std::unique_ptr<PeerListRow> row) override {
// PeerListContentDelegate::peerListAppendRow(std::move(row));
// updateSearchEnabledByContent();
//}
//void peerListPrependRow(
// std::unique_ptr<PeerListRow> row) override {
// PeerListContentDelegate::peerListPrependRow(std::move(row));
// updateSearchEnabledByContent();
//}
//void peerListRemoveRow(not_null<PeerListRow*> row) override {
// PeerListContentDelegate::peerListRemoveRow(row);
// updateSearchEnabledByContent();
//}
void setupHeader();
object_ptr<Ui::FlatLabel> setupTitle();
void setupList();
void setupButtons();
//void updateSearchOverrides();
void addMember();
void showMembersWithSearch(bool withSearch);
//void toggleSearch(anim::type animated = anim::type::normal);
//void cancelSearch();
//void searchAnimationCallback();
void updateHeaderControlsGeometry(int newWidth);
//void updateSearchEnabledByContent();
std::shared_ptr<Main::SessionShow> _show;
//Wrap _wrap;
not_null<Controller*> _controller;
not_null<PeerData*> _peer;
std::unique_ptr<ParticipantsBoxController> _listController;
object_ptr<Ui::RpWidget> _header = { nullptr };
object_ptr<ListWidget> _list = { nullptr };
Ui::SettingsButton *_openMembers = nullptr;
Ui::RpWidget *_titleWrap = nullptr;
Ui::FlatLabel *_title = nullptr;
Ui::IconButton *_addMember = nullptr;
//base::unique_qptr<Ui::InputField> _searchField;
Ui::IconButton *_search = nullptr;
//Ui::CrossButton *_cancelSearch = nullptr;
//Ui::Animations::Simple _searchShownAnimation;
//bool _searchShown = false;
//base::Timer _searchTimer;
rpl::event_stream<Ui::ScrollToRequest> _scrollToRequests;
};
} // namespace Profile
} // namespace Info

View File

@@ -0,0 +1,85 @@
/*
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 "info/profile/info_profile_members_controllers.h"
#include "boxes/peers/edit_participants_box.h"
#include "info/profile/info_profile_values.h"
#include "data/data_chat.h"
#include "data/data_user.h"
#include "ui/unread_badge.h"
#include "lang/lang_keys.h"
#include "styles/style_info.h"
#include "styles/style_boxes.h"
#include "styles/style_dialogs.h"
namespace Info {
namespace Profile {
MemberListRow::MemberListRow(
not_null<UserData*> user,
Type type)
: PeerListRowWithLink(user)
, _type(type) {
setType(type);
}
void MemberListRow::setType(Type type) {
_type = type;
PeerListRowWithLink::setActionLink(!_type.adminRank.isEmpty()
? _type.adminRank
: (_type.rights == Rights::Creator)
? tr::lng_owner_badge(tr::now)
: (_type.rights == Rights::Admin)
? tr::lng_admin_badge(tr::now)
: QString());
}
MemberListRow::Type MemberListRow::type() const {
return _type;
}
bool MemberListRow::rightActionDisabled() const {
return true;
}
QMargins MemberListRow::rightActionMargins() const {
const auto skip = st::contactsCheckPosition.x();
return QMargins(
skip,
st::defaultPeerListItem.namePosition.y(),
st::defaultPeerListItem.photoPosition.x() + skip,
0);
}
not_null<UserData*> MemberListRow::user() const {
return peer()->asUser();
}
void MemberListRow::refreshStatus() {
if (user()->isBot()) {
const auto seesAllMessages = (user()->botInfo->readsAllHistory
|| _type.rights != Rights::Normal);
setCustomStatus(seesAllMessages
? tr::lng_status_bot_reads_all(tr::now)
: tr::lng_status_bot_not_reads_all(tr::now));
} else {
PeerListRow::refreshStatus();
}
}
std::unique_ptr<ParticipantsBoxController> CreateMembersController(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer) {
return std::make_unique<ParticipantsBoxController>(
navigation,
peer,
ParticipantsBoxController::Role::Profile);
}
} // namespace Profile
} // namespace Info

View File

@@ -0,0 +1,54 @@
/*
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 "boxes/peer_list_controllers.h"
#include "ui/unread_badge.h"
class ParticipantsBoxController;
namespace Window {
class SessionNavigation;
} // namespace Window
namespace Info {
namespace Profile {
class MemberListRow final : public PeerListRowWithLink {
public:
enum class Rights {
Normal,
Admin,
Creator,
};
struct Type {
Rights rights;
QString adminRank;
};
MemberListRow(not_null<UserData*> user, Type type);
void setType(Type type);
[[nodiscard]] Type type() const;
bool rightActionDisabled() const override;
QMargins rightActionMargins() const override;
void refreshStatus() override;
not_null<UserData*> user() const;
private:
Type _type;
};
std::unique_ptr<ParticipantsBoxController> CreateMembersController(
not_null<Window::SessionNavigation*> navigation,
not_null<PeerData*> peer);
} // namespace Profile
} // namespace Info

View File

@@ -0,0 +1,142 @@
/*
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 "info/profile/info_profile_music_button.h"
#include "ui/effects/animation_value.h"
#include "ui/text/text_utilities.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/ui_utility.h"
#include "styles/style_chat.h"
#include "styles/style_info.h"
namespace Info::Profile {
MusicButton::MusicButton(
QWidget *parent,
MusicButtonData data,
Fn<void()> handler)
: RippleButton(parent, st::infoMusicButtonRipple)
, _noteSymbol(u"\u266B"_q + QChar(' '))
, _noteWidth(st::normalFont->width(_noteSymbol)) {
updateData(std::move(data));
setClickedCallback(std::move(handler));
}
MusicButton::~MusicButton() = default;
void MusicButton::updateData(MusicButtonData data) {
const auto result = data.name.textWithEntities();
const auto performerLength = result.entities.empty()
? 0
: int(result.entities.front().length());
_performer.setText(
st::semiboldTextStyle,
result.text.mid(0, performerLength));
_title.setText(
st::defaultTextStyle,
result.text.mid(performerLength, result.text.size()));
update();
}
void MusicButton::setOverrideBg(std::optional<QColor> color) {
_overrideBg = color;
update();
}
void MusicButton::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
if (_overrideBg) {
p.fillRect(e->rect(), Ui::BlendColors(
*_overrideBg,
Qt::black,
st::infoProfileTopBarActionButtonBgOpacity));
} else {
p.fillRect(e->rect(), st::shadowFg);
}
paintRipple(p, QPoint());
const auto &icon = st::topicButtonArrow;
const auto iconWidth = icon.width();
const auto iconHeight = icon.height();
const auto padding = st::infoMusicButtonPadding;
const auto skip = st::normalFont->spacew;
const auto titleWidth = _title.maxWidth();
const auto performerWidth = _performer.maxWidth();
const auto totalNeeded = titleWidth + performerWidth + skip;
const auto availableWidth = width()
- rect::m::sum::h(padding)
- iconWidth
- skip
- _noteWidth;
auto actualTitleWidth = 0;
auto actualPerformerWidth = 0;
if (totalNeeded <= availableWidth) {
actualTitleWidth = titleWidth;
actualPerformerWidth = performerWidth;
} else {
const auto ratio = float64(titleWidth) / totalNeeded;
actualPerformerWidth = int(availableWidth * (1.0 - ratio));
actualTitleWidth = availableWidth - actualPerformerWidth;
}
const auto totalContentWidth = _noteWidth
+ actualPerformerWidth
+ skip
+ actualTitleWidth
+ skip
+ iconWidth;
const auto centerX = width() / 2;
const auto contentStartX = centerX - totalContentWidth / 2;
const auto textTop = (height() - st::normalFont->height) / 2;
p.setPen(_overrideBg ? st::groupCallMembersFg : st::windowBoldFg);
p.setFont(st::normalFont);
p.drawText(contentStartX, textTop + st::normalFont->ascent, _noteSymbol);
_performer.draw(p, {
.position = { contentStartX + _noteWidth, textTop },
.availableWidth = actualPerformerWidth,
.now = crl::now(),
.elisionLines = 1,
.elisionMiddle = true,
});
p.setPen(_overrideBg ? st::groupCallVideoSubTextFg : st::windowSubTextFg);
_title.draw(p, {
.position = QPoint(
contentStartX + _noteWidth + actualPerformerWidth + skip,
textTop),
.availableWidth = actualTitleWidth,
.now = crl::now(),
.elisionLines = 1,
.elisionMiddle = true,
});
const auto iconLeft = contentStartX
+ _noteWidth
+ actualPerformerWidth
+ actualTitleWidth
+ skip
+ skip;
const auto iconTop = (height() - iconHeight) / 2;
icon.paint(p, iconLeft, iconTop, iconWidth, p.pen().color());
}
int MusicButton::resizeGetHeight(int newWidth) {
const auto padding = st::infoMusicButtonPadding;
const auto &font = st::defaultTextStyle.font;
return padding.top() + font->height + padding.bottom();
}
} // namespace Info::Profile

View File

@@ -0,0 +1,41 @@
/*
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 "ui/widgets/buttons.h"
#include "ui/text/format_song_name.h"
#include "ui/text/text.h"
namespace Info::Profile {
struct MusicButtonData {
Ui::Text::FormatSongName name;
};
class MusicButton final : public Ui::RippleButton {
public:
MusicButton(QWidget *parent, MusicButtonData data, Fn<void()> handler);
~MusicButton();
void updateData(MusicButtonData data);
void setOverrideBg(std::optional<QColor> color);
private:
void paintEvent(QPaintEvent *e) override;
int resizeGetHeight(int newWidth) override;
Ui::Text::String _performer;
Ui::Text::String _title;
std::optional<QColor> _overrideBg;
const QString _noteSymbol;
const int _noteWidth;
};
} // namespace Info::Profile

View File

@@ -0,0 +1,146 @@
/*
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 "info/profile/info_profile_phone_menu.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_app_config_values.h"
#include "main/main_session.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/menu/menu_action.h"
#include "ui/widgets/popup_menu.h"
#include "styles/style_chat.h" // expandedMenuSeparator.
#include "styles/style_chat_helpers.h"
namespace Info {
namespace Profile {
namespace {
class TextItem final : public Ui::Menu::ItemBase {
public:
TextItem(
not_null<Ui::RpWidget*> parent,
const style::Menu &st,
rpl::producer<TextWithEntities> &&text);
not_null<QAction*> action() const override;
bool isEnabled() const override;
protected:
int contentHeight() const override;
private:
const base::unique_qptr<Ui::FlatLabel> _label;
const not_null<QAction*> _dummyAction;
};
[[nodiscard]] int CountMinWidthForHeight(
not_null<Ui::FlatLabel*> label,
int basicWidth,
int heightLimit) {
const auto height = [&](int width) {
label->resizeToWidth(width);
return label->height();
};
auto widthMin = basicWidth;
auto widthMax = label->textMaxWidth();
if (height(widthMin) <= heightLimit || height(widthMax) > heightLimit) {
return basicWidth;
}
while (widthMin + 1 < widthMax) {
const auto middle = (widthMin + widthMax) / 2;
if (height(middle) > heightLimit) {
widthMin = middle;
} else {
widthMax = middle;
}
}
return widthMax;
}
TextItem::TextItem(
not_null<Ui::RpWidget*> parent,
const style::Menu &st,
rpl::producer<TextWithEntities> &&text)
: ItemBase(parent, st)
, _label(base::make_unique_q<Ui::FlatLabel>(
this,
std::move(text),
st::historyMessagesTTLLabel))
, _dummyAction(Ui::CreateChild<QAction>(parent.get())) {
// Try to fit the phrase in two lines.
const auto limit = st::historyMessagesTTLLabel.style.font->height * 2;
const auto min1 = st::historyMessagesTTLLabel.minWidth;
const auto min2 = CountMinWidthForHeight(_label.get(), min1, limit);
const auto added = st.itemPadding.left() + st.itemPadding.right();
setMinWidth(std::max(min1, min2) + added);
sizeValue(
) | rpl::on_next([=](const QSize &s) {
if (s.width() <= added) {
return;
}
_label->resizeToWidth(s.width() - added);
_label->moveToLeft(
st.itemPadding.left(),
(s.height() - _label->height()) / 2);
}, lifetime());
_label->resizeToWidth(parent->width() - added);
initResizeHook(parent->sizeValue());
}
not_null<QAction*> TextItem::action() const {
return _dummyAction;
}
bool TextItem::isEnabled() const {
return false;
}
int TextItem::contentHeight() const {
return _label->height();
}
} // namespace
bool IsCollectiblePhone(not_null<UserData*> user) {
using Strings = std::vector<QString>;
const auto prefixes = user->session().appConfig().get<Strings>(
u"fragment_prefixes"_q,
Strings{ u"888"_q });
const auto phone = user->phone();
const auto proj = [&](const QString &p) {
return phone.startsWith(p);
};
return ranges::any_of(prefixes, proj);
}
void AddPhoneMenu(not_null<Ui::PopupMenu*> menu, not_null<UserData*> user) {
if (user->isSelf() || !IsCollectiblePhone(user)) {
return;
} else if (const auto url = AppConfig::FragmentLink(&user->session())) {
menu->addSeparator(&st::expandedMenuSeparator);
const auto link = tr::link(
tr::lng_info_mobile_context_menu_fragment_about_link(tr::now),
*url);
menu->addAction(base::make_unique_q<TextItem>(
menu->menu(),
st::reactionMenu.menu,
tr::lng_info_mobile_context_menu_fragment_about(
lt_link,
rpl::single(link),
tr::rich)));
}
}
} // namespace Profile
} // namespace Info

View File

@@ -0,0 +1,24 @@
/*
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
class UserData;
namespace Ui {
class PopupMenu;
} // namespace Ui
namespace Info {
namespace Profile {
[[nodiscard]] bool IsCollectiblePhone(not_null<UserData*> user);
void AddPhoneMenu(not_null<Ui::PopupMenu*> menu, not_null<UserData*> user);
} // namespace Profile
} // namespace Info

View File

@@ -0,0 +1,173 @@
/*
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 "info/profile/info_profile_status_label.h"
#include "data/data_peer_values.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_peer.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "ui/widgets/labels.h"
#include "ui/text/text_utilities.h"
#include "ui/basic_click_handlers.h"
#include "base/unixtime.h"
namespace Info::Profile {
namespace {
[[nodiscard]] auto MembersStatusText(int count) {
return tr::lng_chat_status_members(tr::now, lt_count_decimal, count);
};
[[nodiscard]] auto OnlineStatusText(int count) {
return tr::lng_chat_status_online(tr::now, lt_count_decimal, count);
};
[[nodiscard]] auto ChatStatusText(
int fullCount,
int onlineCount,
bool isGroup) {
if (onlineCount > 1 && onlineCount <= fullCount) {
return tr::lng_chat_status_members_online(
tr::now,
lt_members_count,
MembersStatusText(fullCount),
lt_online_count,
OnlineStatusText(onlineCount));
} else if (fullCount > 0) {
return isGroup
? tr::lng_chat_status_members(
tr::now,
lt_count_decimal,
fullCount)
: tr::lng_chat_status_subscribers(
tr::now,
lt_count_decimal,
fullCount);
}
return isGroup
? tr::lng_group_status(tr::now)
: tr::lng_channel_status(tr::now);
};
[[nodiscard]] auto ChannelTypeText(not_null<ChannelData*> channel) {
const auto isPublic = channel->isPublic();
const auto isMegagroup = channel->isMegagroup();
return (isPublic
? (isMegagroup
? tr::lng_create_public_group_title(tr::now)
: tr::lng_create_public_channel_title(tr::now))
: (isMegagroup
? tr::lng_create_private_group_title(tr::now)
: tr::lng_create_private_channel_title(tr::now))).toLower();
};
} // namespace
StatusLabel::StatusLabel(
not_null<Ui::FlatLabel*> label,
not_null<PeerData*> peer)
: _label(label)
, _peer(peer)
, _refreshTimer([=] { refresh(); }) {
}
void StatusLabel::setOnlineCount(int count) {
_onlineCount = count;
refresh();
}
void StatusLabel::refresh() {
auto hasMembersLink = [&] {
if (auto megagroup = _peer->asMegagroup()) {
return megagroup->canViewMembers();
}
return false;
}();
auto statusText = [&]() -> TextWithEntities {
using namespace Ui::Text;
auto currentTime = base::unixtime::now();
if (auto user = _peer->asUser()) {
const auto result = Data::OnlineTextFull(user, currentTime);
const auto showOnline = Data::OnlineTextActive(
user,
currentTime);
const auto updateIn = Data::OnlineChangeTimeout(
user,
currentTime);
if (showOnline) {
_refreshTimer.callOnce(updateIn);
}
return (showOnline && _colorized)
? Ui::Text::Colorized(result)
: TextWithEntities{ .text = result };
} else if (auto chat = _peer->asChat()) {
if (!chat->amIn()) {
return tr::lng_chat_status_unaccessible(
{},
WithEntities);
}
const auto onlineCount = _onlineCount;
const auto fullCount = std::max(
chat->count,
int(chat->participants.size()));
return { .text = ChatStatusText(
fullCount,
onlineCount,
true) };
} else if (auto broadcast = _peer->monoforumBroadcast()) {
if (!broadcast->membersCountKnown()) {
return TextWithEntities{ .text = ChannelTypeText(broadcast) };
}
auto result = ChatStatusText(
broadcast->membersCount(),
0,
false);
return TextWithEntities{ .text = result };
} else if (auto channel = _peer->asChannel()) {
if (!channel->membersCountKnown()) {
auto result = ChannelTypeText(channel);
return hasMembersLink
? tr::link(result)
: TextWithEntities{ .text = result };
}
const auto onlineCount = _onlineCount;
const auto fullCount = channel->membersCount();
auto result = ChatStatusText(
fullCount,
onlineCount,
channel->isMegagroup());
return hasMembersLink
? tr::link(result)
: TextWithEntities{ .text = result };
}
return tr::lng_chat_status_unaccessible(tr::now, WithEntities);
}();
_label->setMarkedText(statusText);
if (hasMembersLink && _membersLinkCallback) {
_label->setLink(
1,
std::make_shared<LambdaClickHandler>(_membersLinkCallback));
}
}
void StatusLabel::setMembersLinkCallback(Fn<void()> callback) {
_membersLinkCallback = std::move(callback);
}
Fn<void()> StatusLabel::membersLinkCallback() const {
return _membersLinkCallback;
}
void StatusLabel::setColorized(bool enabled) {
_colorized = enabled;
refresh();
}
} // namespace Info::Profile

View File

@@ -0,0 +1,40 @@
/*
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 "base/timer.h"
namespace Ui {
class FlatLabel;
} // namespace Ui
namespace Info::Profile {
class StatusLabel final {
public:
StatusLabel(
not_null<Ui::FlatLabel*> label,
not_null<PeerData*> peer);
void refresh();
void setMembersLinkCallback(Fn<void()> callback);
[[nodiscard]] Fn<void()> membersLinkCallback() const;
void setOnlineCount(int count);
void setColorized(bool enabled);
private:
const not_null<Ui::FlatLabel*> _label;
const not_null<PeerData*> _peer;
int _onlineCount = 0;
bool _colorized = true;
Fn<void()> _membersLinkCallback;
base::Timer _refreshTimer;
};
} // namespace Info::Profile

View File

@@ -0,0 +1,69 @@
/*
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 "info/profile/info_profile_text.h"
#include <rpl/before_next.h>
#include <rpl/filter.h>
#include <rpl/after_next.h>
#include "ui/widgets/labels.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "styles/style_info.h"
namespace Info {
namespace Profile {
TextWithLabel CreateTextWithLabel(
QWidget *parent,
rpl::producer<TextWithEntities> &&label,
rpl::producer<TextWithEntities> &&text,
const style::FlatLabel &labelSt,
const style::FlatLabel &textSt,
const style::margins &padding) {
auto result = object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
parent,
object_ptr<Ui::VerticalLayout>(parent),
padding);
result->setDuration(st::infoSlideDuration);
auto layout = result->entity();
auto nonEmptyText = rpl::duplicate(
text
) | rpl::before_next([slide = result.data()](
const TextWithEntities &value) {
if (value.text.isEmpty()) {
slide->hide(anim::type::normal);
}
}) | rpl::filter([](const TextWithEntities &value) {
return !value.text.isEmpty();
}) | rpl::after_next([slide = result.data()](
const TextWithEntities &value) {
slide->show(anim::type::normal);
});
const auto labeled = layout->add(object_ptr<Ui::FlatLabel>(
layout,
std::move(nonEmptyText),
textSt));
std::move(text) | rpl::on_next([=] {
labeled->resizeToWidth(layout->width());
}, labeled->lifetime());
labeled->setSelectable(true);
layout->add(Ui::CreateSkipWidget(layout, st::infoLabelSkip));
const auto subtext = layout->add(object_ptr<Ui::FlatLabel>(
layout,
std::move(
label
) | rpl::after_next([=] {
layout->resizeToWidth(layout->widthNoMargins());
}),
labelSt));
result->finishAnimating();
return { std::move(result), labeled, subtext };
}
} // namespace Profile
} // namespace Info

View File

@@ -0,0 +1,43 @@
/*
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 "base/object_ptr.h"
#include <rpl/producer.h>
namespace style {
struct FlatLabel;
} // namespace style
namespace Ui {
class VerticalLayout;
class FlatLabel;
template <typename Widget>
class SlideWrap;
} // namespace Ui
namespace Info {
namespace Profile {
struct TextWithLabel {
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>> wrap;
not_null<Ui::FlatLabel*> text;
not_null<Ui::FlatLabel*> subtext;
};
TextWithLabel CreateTextWithLabel(
QWidget *parent,
rpl::producer<TextWithEntities> &&label,
rpl::producer<TextWithEntities> &&text,
const style::FlatLabel &labelSt,
const style::FlatLabel &textSt,
const style::margins &padding);
} // namespace Profile
} // namespace Info

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,299 @@
/*
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 "base/object_ptr.h"
#include "info/info_controller.h" // Key
#include "info/profile/info_profile_badge.h"
#include "ui/rp_widget.h"
#include "ui/userpic_view.h"
namespace Data {
class ForumTopic;
class DocumentMedia;
struct SavedStarGift;
struct ColorProfileSet;
class SavedStarGiftId;
} // namespace Data
namespace Info::Profile {
class BadgeTooltip;
class TopicIconView;
} // namespace Info::Profile
namespace Lottie {
class Animation;
class MultiPlayer;
} // namespace Lottie
namespace Ui {
class VideoUserpicPlayer;
struct OutlineSegment;
namespace Text {
class CustomEmoji;
} // namespace Text
} // namespace Ui
class PeerData;
namespace base {
class Timer;
} // namespace base
namespace style {
struct InfoTopBar;
struct InfoPeerBadge;
struct FlatLabel;
} //namespace style
class QGraphicsOpacityEffect;
namespace Ui {
class FlatLabel;
class IconButton;
class PopupMenu;
class RoundButton;
class StarsRating;
template <typename Widget>
class FadeWrap;
class HorizontalFitContainer;
namespace Animations {
class Simple;
} // namespace Animations
} //namespace Ui
namespace Info {
class Controller;
class Key;
enum class Wrap;
} //namespace Info
namespace Ui::Menu {
struct MenuCallback;
} //namespace Ui::Menu
namespace Info::Profile {
class Badge;
class StatusLabel;
struct TopBarActionButtonStyle;
class TopBar final : public Ui::RpWidget {
public:
enum class Source {
Profile,
Stories,
Preview,
};
struct Descriptor {
not_null<Window::SessionController*> controller;
Key key;
rpl::producer<Wrap> wrap;
Source source = Source::Profile;
PeerData *peer = nullptr;
rpl::producer<bool> backToggles;
rpl::producer<> showFinished;
};
struct AnimatedPatternPoint {
QPointF basePosition;
float64 size;
float64 startTime;
float64 endTime;
};
TopBar(not_null<Ui::RpWidget*> parent, Descriptor descriptor);
~TopBar();
[[nodiscard]] rpl::producer<> backRequest() const;
void setOnlineCount(rpl::producer<int> &&count);
void setRoundEdges(bool value);
void setLottieSingleLoop(bool value);
void setColorProfileIndex(std::optional<uint8> index);
void setPatternEmojiId(std::optional<DocumentId> patternEmojiId);
void setLocalEmojiStatusId(EmojiStatusId emojiStatusId);
void addTopBarEditButton(
not_null<Window::SessionController*> controller,
Wrap wrap,
bool shouldUseColored);
rpl::producer<std::optional<QColor>> edgeColor() const;
protected:
void resizeEvent(QResizeEvent *e) override;
void paintEvent(QPaintEvent *e) override;
private:
void paintEdges(QPainter &p, const QBrush &brush) const;
void paintEdges(QPainter &p) const;
void updateLabelsPosition();
[[nodiscard]] int titleMostLeft() const;
[[nodiscard]] int statusMostLeft() const;
[[nodiscard]] QRect userpicGeometry() const;
void updateGiftButtonsGeometry(
float64 progressCurrent,
const QRect &userpicRect);
void paintUserpic(QPainter &p, const QRect &geometry);
void updateVideoUserpic();
void showTopBarMenu(
not_null<Window::SessionController*> controller,
bool check);
void fillTopBarMenu(
not_null<Window::SessionController*> controller,
const Ui::Menu::MenuCallback &addAction);
void setupUserpicButton(not_null<Window::SessionController*> controller);
void setupActions(not_null<Window::SessionController*> controller);
void setupButtons(
not_null<Window::SessionController*> controller,
Source source);
void setupShowLastSeen(not_null<Window::SessionController*> controller);
void setupUniqueBadgeTooltip();
void hideBadgeTooltip();
void setupAnimatedPattern(const QRect &userpicGeometry = QRect());
void paintAnimatedPattern(
QPainter &p,
const QRect &rect,
const QRect &userpicGeometry);
void setupPinnedToTopGifts(
not_null<Window::SessionController*> controller);
void setupNewGifts(
not_null<Window::SessionController*> controller,
const std::vector<Data::SavedStarGift> &gifts);
void paintPinnedToTopGifts(
QPainter &p,
const QRect &rect,
const QRect &userpicGeometry);
[[nodiscard]] QPointF calculateGiftPosition(
int position,
float64 progress,
const QRect &userpicRect) const;
void adjustColors(const std::optional<QColor> &edgeColor);
void updateCollectibleStatus();
void setupStoryOutline(const QRect &geometry = QRect());
void updateStoryOutline(std::optional<QColor> edgeColor);
void paintStoryOutline(QPainter &p, const QRect &geometry);
void updateStatusPosition(float64 progressCurrent);
[[nodiscard]] int calculateRightButtonsWidth() const;
[[nodiscard]] const style::FlatLabel &statusStyle() const;
void setupStatusWithRating();
[[nodiscard]] TopBarActionButtonStyle mapActionStyle(
std::optional<QColor> c) const;
[[nodiscard]] rpl::producer<QString> nameValue() const;
[[nodiscard]] auto effectiveColorProfile()
const -> std::optional<Data::ColorProfileSet>;
[[nodiscard]] auto effectiveCollectible()
const -> std::shared_ptr<Data::EmojiStatusCollectible>;
const not_null<PeerData*> _peer;
Data::ForumTopic *_topic = nullptr;
const Key _key;
rpl::variable<Wrap> _wrap;
const style::InfoTopBar &_st;
const Source _source;
std::unique_ptr<base::Timer> _badgeTooltipHide;
const std::unique_ptr<Badge> _botVerify;
rpl::variable<Badge::Content> _badgeContent;
const Fn<bool()> _gifPausedChecker;
const std::unique_ptr<Badge> _badge;
const std::unique_ptr<Badge> _verified;
const bool _hasActions;
const int _minForProgress;
std::unique_ptr<BadgeTooltip> _badgeTooltip;
std::vector<std::unique_ptr<BadgeTooltip>> _badgeOldTooltips;
uint64 _badgeCollectibleId = 0;
object_ptr<Ui::FlatLabel> _title;
std::unique_ptr<Ui::StarsRating> _starsRating;
object_ptr<Ui::FlatLabel> _status;
std::unique_ptr<StatusLabel> _statusLabel;
rpl::variable<int> _statusShift = 0;
object_ptr<Ui::RoundButton> _showLastSeen = { nullptr };
object_ptr<Ui::RoundButton> _forumButton = { nullptr };
QGraphicsOpacityEffect *_showLastSeenOpacity = nullptr;
std::shared_ptr<style::FlatLabel> _statusSt;
std::shared_ptr<style::InfoPeerBadge> _botVerifySt;
std::shared_ptr<style::InfoPeerBadge> _badgeSt;
std::shared_ptr<style::InfoPeerBadge> _verifiedSt;
rpl::variable<float64> _progress = 0.;
bool _roundEdges = true;
rpl::variable<std::optional<QColor>> _edgeColor;
bool _hasGradientBg = false;
std::optional<QColor> _solidBg;
QImage _cachedGradient;
QPainterPath _cachedClipPath;
std::unique_ptr<Ui::Text::CustomEmoji> _patternEmoji;
QImage _basePatternImage;
std::vector<AnimatedPatternPoint> _animatedPoints;
QRect _lastUserpicRect;
base::unique_qptr<Ui::AbstractButton> _userpicButton;
Ui::PeerUserpicView _userpicView;
InMemoryKey _userpicUniqueKey;
QImage _cachedUserpic;
QImage _monoforumMask;
std::unique_ptr<Ui::VideoUserpicPlayer> _videoUserpicPlayer;
std::unique_ptr<TopicIconView> _topicIconView;
rpl::lifetime _userpicLoadingLifetime;
base::unique_qptr<Ui::IconButton> _close;
base::unique_qptr<Ui::FadeWrap<Ui::IconButton>> _back;
rpl::variable<bool> _backToggles;
rpl::event_stream<> _backClicks;
base::unique_qptr<Ui::IconButton> _topBarButton;
base::unique_qptr<Ui::PopupMenu> _peerMenu;
Ui::RpWidget *_actionMore = nullptr;
base::unique_qptr<Ui::HorizontalFitContainer> _actions;
std::unique_ptr<Lottie::MultiPlayer> _lottiePlayer;
bool _lottieSingleLoop = false;
struct PinnedToTopGiftEntry {
Data::SavedStarGiftId manageId;
// QString slug;
Lottie::Animation *animation = nullptr;
std::shared_ptr<Data::DocumentMedia> media;
QImage bg;
QImage lastFrame;
int position = 0;
base::unique_qptr<Ui::AbstractButton> button;
};
bool _pinnedToTopGiftsFirstTimeShowed = false;
std::vector<PinnedToTopGiftEntry> _pinnedToTopGifts;
std::unique_ptr<Ui::Animations::Simple> _giftsAppearing;
std::unique_ptr<Ui::Animations::Simple> _giftsHiding;
rpl::lifetime _giftsLoadingLifetime;
QBrush _storyOutlineBrush;
std::vector<Ui::OutlineSegment> _storySegments;
bool _hasStories = false;
bool _hasLiveStories = false;
std::optional<uint8> _localColorProfileIndex;
std::optional<DocumentId> _localPatternEmojiId;
std::shared_ptr<Data::EmojiStatusCollectible> _localCollectible;
};
} // namespace Info::Profile

View File

@@ -0,0 +1,191 @@
/*
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 "info/profile/info_profile_top_bar_action_button.h"
#include "ui/effects/ripple_animation.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "lottie/lottie_icon.h"
#include "styles/style_layers.h"
#include "styles/style_info.h"
namespace Info::Profile {
namespace {
constexpr auto kIconFadeStart = 0.4;
constexpr auto kIconFadeRange = 1.0 - kIconFadeStart;
} // namespace
TopBarActionButton::TopBarActionButton(
not_null<QWidget*> parent,
const QString &text,
const QString &lottieName)
: RippleButton(parent, st::universalRippleAnimation)
, _text(text) {
setupLottie(lottieName);
}
TopBarActionButton::TopBarActionButton(
not_null<QWidget*> parent,
const QString &text,
const style::icon &icon)
: RippleButton(parent, st::universalRippleAnimation)
, _text(text)
, _icon(&icon) {
}
TopBarActionButton::~TopBarActionButton() = default;
void TopBarActionButton::setupLottie(const QString &lottieName) {
_lottie = std::make_unique<Lottie::Icon>(Lottie::IconDescriptor{
.name = lottieName,
.color = _lottieColor,
.sizeOverride = Size(st::infoProfileTopBarActionButtonLottieSize),
});
_lottie->animate([=] { update(); }, 0, _lottie->framesCount() - 1);
}
void TopBarActionButton::convertToToggle(
const style::icon &offIcon,
const style::icon &onIcon,
const QString &offLottie,
const QString &onLottie) {
_isToggle = true;
_offIcon = &offIcon;
_onIcon = &onIcon;
_offLottie = offLottie;
_onLottie = onLottie;
_icon = _offIcon;
_lottie.reset();
}
void TopBarActionButton::toggle(bool state) {
if (!_isToggle || _toggleState == state) {
return;
}
_toggleState = state;
const auto &lottie = _toggleState ? _onLottie : _offLottie;
setupLottie(lottie);
_lottie->animate([=] {
update();
if (_lottie->frameIndex() == _lottie->framesCount() - 1) {
_icon = _toggleState ? _onIcon : _offIcon;
_lottie.reset();
}
}, 0, _lottie->framesCount() - 1);
}
void TopBarActionButton::finishAnimating() {
if (_lottie) {
_icon = _toggleState ? _onIcon : _offIcon;
_lottie.reset();
update();
}
}
void TopBarActionButton::setText(const QString &text) {
_text = text;
update();
}
void TopBarActionButton::setLottieColor(const style::color *color) {
_lottieColor = color;
_lottie.reset();
update();
}
void TopBarActionButton::setStyle(const TopBarActionButtonStyle &style) {
_bgColor = style.bgColor;
_fgColor = style.fgColor;
_shadowColor = style.shadowColor;
update();
}
void TopBarActionButton::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
const auto progress = float64(height())
/ st::infoProfileTopBarActionButtonSize;
p.setOpacity(progress);
p.setPen(Qt::NoPen);
p.setBrush(_bgColor);
{
auto hq = PainterHighQualityEnabler(p);
// Todo shadows.
p.drawRoundedRect(rect(), st::boxRadius, st::boxRadius);
}
paintRipple(p, 0, 0);
const auto iconSize = st::infoProfileTopBarActionButtonIconSize;
const auto iconTop = st::infoProfileTopBarActionButtonIconTop;
if (_lottie || _icon) {
const auto iconScale = (progress > kIconFadeStart)
? (progress - kIconFadeStart) / kIconFadeRange
: 0.0;
p.setOpacity(iconScale);
p.save();
const auto iconLeft = (width() - iconSize) / 2.;
const auto half = iconSize / 2.;
const auto iconCenter = QPointF(iconLeft + half, iconTop + half);
p.translate(iconCenter);
p.scale(iconScale, iconScale);
p.translate(-iconCenter);
p.translate(iconLeft, iconTop);
if (_lottie) {
_lottie->paint(p, 0, 0, _fgColor);
} else if (_icon) {
if (_fgColor) {
_icon->paint(p, 0, 0, width(), *_fgColor);
} else {
_icon->paint(p, 0, 0, width());
}
}
p.restore();
p.setOpacity(progress);
}
const auto skip = st::infoProfileTopBarActionButtonTextSkip;
p.setClipRect(0, 0, width(), height() - skip);
if (_fgColor.has_value()) {
p.setPen(*_fgColor);
} else {
p.setPen(st::windowBoldFg);
}
p.setFont(st::infoProfileTopBarActionButtonFont);
const auto textScale = std::max(kIconFadeStart, progress);
const auto textRect = rect()
- QMargins(0, st::infoProfileTopBarActionButtonTextTop, 0, 0);
const auto textCenter = rect::center(textRect);
p.translate(textCenter);
p.scale(textScale, textScale);
p.translate(-textCenter);
const auto elidedText = st::infoProfileTopBarActionButtonFont->elided(
_text,
textRect.width(),
Qt::ElideMiddle);
p.drawText(textRect, elidedText, style::al_top);
}
QImage TopBarActionButton::prepareRippleMask() const {
return Ui::RippleAnimation::RoundRectMask(size(), st::boxRadius);
}
QPoint TopBarActionButton::prepareRippleStartPosition() const {
return mapFromGlobal(QCursor::pos());
}
} // namespace Info::Profile

View File

@@ -0,0 +1,75 @@
/*
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 "ui/widgets/buttons.h"
namespace Lottie {
class Icon;
} // namespace Lottie
namespace Info::Profile {
struct TopBarActionButtonStyle {
QColor bgColor;
std::optional<QColor> fgColor;
std::optional<QColor> shadowColor;
};
class TopBarActionButton final : public Ui::RippleButton {
public:
TopBarActionButton(
not_null<QWidget*> parent,
const QString &text,
const QString &lottieName);
TopBarActionButton(
not_null<QWidget*> parent,
const QString &text,
const style::icon &icon);
void convertToToggle(
const style::icon &offIcon,
const style::icon &onIcon,
const QString &offLottie,
const QString &onLottie);
void setLottieColor(const style::color *color);
void toggle(bool state);
void finishAnimating();
void setText(const QString &text);
void setStyle(const TopBarActionButtonStyle &style);
~TopBarActionButton();
protected:
void paintEvent(QPaintEvent *e) override;
QImage prepareRippleMask() const override;
QPoint prepareRippleStartPosition() const override;
private:
void setupLottie(const QString &lottieName);
QString _text;
std::unique_ptr<Lottie::Icon> _lottie;
const style::icon *_icon = nullptr;
bool _isToggle = false;
bool _toggleState = false;
const style::icon *_offIcon = nullptr;
const style::icon *_onIcon = nullptr;
QString _offLottie;
QString _onLottie;
const style::color *_lottieColor = nullptr;
QColor _bgColor;
std::optional<QColor> _fgColor;
std::optional<QColor> _shadowColor;
};
} // namespace Info::Profile

View File

@@ -0,0 +1,773 @@
/*
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 "info/profile/info_profile_values.h"
#include "api/api_chat_participants.h"
#include "apiwrap.h"
#include "info/profile/info_profile_phone_menu.h"
#include "info/profile/info_profile_badge.h"
#include "core/application.h"
#include "core/click_handler_types.h"
#include "countries/countries_instance.h"
#include "main/main_session.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/text/format_values.h" // Ui::FormatPhone
#include "ui/text/text_utilities.h"
#include "lang/lang_keys.h"
#include "data/notify/data_notify_settings.h"
#include "data/data_peer_values.h"
#include "data/data_saved_messages.h"
#include "data/data_saved_sublist.h"
#include "data/data_shared_media.h"
#include "data/data_message_reactions.h"
#include "data/data_folder.h"
#include "data/data_changes.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_user.h"
#include "data/data_forum_topic.h"
#include "data/data_session.h"
#include "data/data_premium_limits.h"
#include "boxes/peers/edit_peer_permissions_box.h"
#include "base/unixtime.h"
namespace Info {
namespace Profile {
namespace {
using UpdateFlag = Data::PeerUpdate::Flag;
auto PlainAboutValue(not_null<PeerData*> peer) {
return peer->session().changes().peerFlagsValue(
peer,
UpdateFlag::About
) | rpl::map([=] {
return peer->about();
});
}
auto PlainUsernameValue(not_null<PeerData*> peer) {
return rpl::merge(
peer->session().changes().peerFlagsValue(peer, UpdateFlag::Username),
peer->session().changes().peerFlagsValue(peer, UpdateFlag::Usernames)
) | rpl::map([=] {
return peer->username();
});
}
auto PlainPrimaryUsernameValue(not_null<PeerData*> peer) {
return UsernamesValue(
peer
) | rpl::map([=](std::vector<TextWithEntities> usernames) {
if (!usernames.empty()) {
return rpl::single(usernames.front().text) | rpl::type_erased;
} else {
return PlainUsernameValue(peer) | rpl::type_erased;
}
}) | rpl::flatten_latest();
}
void StripExternalLinks(TextWithEntities &text) {
const auto local = [](const QString &url) {
return !UrlRequiresConfirmation(QUrl::fromUserInput(url));
};
const auto notLocal = [&](const EntityInText &entity) {
if (entity.type() == EntityType::CustomUrl) {
return !local(entity.data());
} else if (entity.type() == EntityType::Url) {
return !local(text.text.mid(entity.offset(), entity.length()));
} else {
return false;
}
};
text.entities.erase(
ranges::remove_if(text.entities, notLocal),
text.entities.end());
}
} // namespace
rpl::producer<QString> NameValue(not_null<PeerData*> peer) {
if (const auto broadcast = peer->monoforumBroadcast()) {
return NameValue(broadcast);
}
return peer->session().changes().peerFlagsValue(
peer,
UpdateFlag::Name
) | rpl::map([=] { return peer->name(); });
}
rpl::producer<QString> TitleValue(not_null<Data::ForumTopic*> topic) {
return topic->session().changes().topicFlagsValue(
topic,
Data::TopicUpdate::Flag::Title
) | rpl::map([=] { return topic->title(); });
}
rpl::producer<DocumentId> IconIdValue(not_null<Data::ForumTopic*> topic) {
return topic->session().changes().topicFlagsValue(
topic,
Data::TopicUpdate::Flag::IconId
) | rpl::map([=] { return topic->iconId(); });
}
rpl::producer<int32> ColorIdValue(not_null<Data::ForumTopic*> topic) {
return topic->session().changes().topicFlagsValue(
topic,
Data::TopicUpdate::Flag::ColorId
) | rpl::map([=] { return topic->colorId(); });
}
rpl::producer<TextWithEntities> PhoneValue(not_null<UserData*> user) {
return rpl::merge(
Countries::Instance().updated(),
user->session().changes().peerFlagsValue(
user,
UpdateFlag::PhoneNumber) | rpl::to_empty
) | rpl::map([=] {
return tr::marked(Ui::FormatPhone(user->phone()));
});
}
rpl::producer<TextWithEntities> PhoneOrHiddenValue(not_null<UserData*> user) {
return rpl::combine(
PhoneValue(user),
PlainUsernameValue(user),
PlainAboutValue(user),
tr::lng_info_mobile_hidden()
) | rpl::map([user](
const TextWithEntities &phone,
const QString &username,
const QString &about,
const QString &hidden) {
if (phone.text.isEmpty() && username.isEmpty() && about.isEmpty()) {
return tr::marked(hidden);
} else if (IsCollectiblePhone(user)) {
return tr::link(phone, u"internal:collectible_phone/"_q
+ user->phone() + '@' + QString::number(user->id.value));
} else {
return phone;
}
});
}
rpl::producer<TextWithEntities> UsernameValue(
not_null<PeerData*> peer,
bool primary) {
return (primary
? PlainPrimaryUsernameValue(peer)
: (PlainUsernameValue(peer) | rpl::type_erased)
) | rpl::map([](QString &&username) {
return username.isEmpty()
? tr::marked()
: tr::marked('@' + username);
});
}
QString UsernameUrl(
not_null<PeerData*> peer,
const QString &username,
bool link) {
const auto type = !peer->isUsernameEditable(username)
? u"collectible_username"_q
: link
? u"username_link"_q
: u"username_regular"_q;
return u"internal:"_q
+ type
+ u"/"_q
+ username
+ "@"
+ QString::number(peer->id.value);
}
rpl::producer<std::vector<TextWithEntities>> UsernamesValue(
not_null<PeerData*> peer) {
const auto map = [=](const std::vector<QString> &usernames) {
return ranges::views::all(
usernames
) | ranges::views::transform([&](const QString &u) {
return tr::link(u, UsernameUrl(peer, u));
}) | ranges::to_vector;
};
auto value = rpl::merge(
peer->session().changes().peerFlagsValue(peer, UpdateFlag::Username),
peer->session().changes().peerFlagsValue(peer, UpdateFlag::Usernames)
);
if (const auto user = peer->asUser()) {
return std::move(value) | rpl::map([=] {
return map(user->usernames());
});
} else if (const auto channel = peer->asChannel()) {
return std::move(value) | rpl::map([=] {
return map(channel->usernames());
});
} else {
return rpl::single(std::vector<TextWithEntities>());
}
}
TextWithEntities AboutWithEntities(
not_null<PeerData*> peer,
const QString &value) {
auto flags = TextParseLinks | TextParseMentions;
const auto user = peer->asUser();
const auto isBot = user && user->isBot();
const auto isPremium = user && user->isPremium();
if (!user) {
flags |= TextParseHashtags;
} else if (isBot) {
flags |= TextParseHashtags | TextParseBotCommands;
}
const auto stripExternal = peer->isChat()
|| peer->isMegagroup()
|| (user && !isBot && !isPremium);
auto result = TextWithEntities{ value };
TextUtilities::ParseEntities(result, flags);
if (stripExternal) {
StripExternalLinks(result);
}
return result;
}
rpl::producer<TextWithEntities> AboutValue(not_null<PeerData*> peer) {
return PlainAboutValue(
peer
) | rpl::map([peer](const QString &value) {
return AboutWithEntities(peer, value);
});
}
QString TopicLink(not_null<Data::ForumTopic*> topic, bool full) {
const auto channel = topic->channel();
const auto id = topic->rootId();
const auto base = channel->hasUsername()
? channel->username()
: "c/" + QString::number(peerToChannel(channel->id).bare);
return channel->session().createInternalLinkFull(full
? base + '/' + QString::number(id.bare)
: base);
}
rpl::producer<LinkWithUrl> LinkValue(
not_null<PeerData*> peer,
bool primary,
MsgId rootId) {
return (primary
? PlainPrimaryUsernameValue(peer)
: PlainUsernameValue(peer) | rpl::type_erased
) | rpl::map([=](QString &&username) {
if (username.isEmpty()) {
if (const auto topic
= rootId ? peer->forumTopicFor(rootId) : nullptr) {
const auto link = TopicLink(topic, false);
return LinkWithUrl{
.text = link,
.url = link,
};
} else {
return LinkWithUrl{};
}
} else {
return LinkWithUrl{
.text = peer->session().createInternalLinkFull(username),
.url = UsernameUrl(peer, username, true),
};
}
return LinkWithUrl{
.text = (username.isEmpty()
? QString()
: peer->session().createInternalLinkFull(username)),
.url = (username.isEmpty()
? QString()
: UsernameUrl(peer, username, true)),
};
});
}
rpl::producer<const ChannelLocation*> LocationValue(
not_null<ChannelData*> channel) {
return channel->session().changes().peerFlagsValue(
channel,
UpdateFlag::ChannelLocation
) | rpl::map([=] {
return channel->getLocation();
});
}
rpl::producer<bool> NotificationsEnabledValue(
not_null<Data::Thread*> thread) {
const auto topic = thread->asTopic();
if (!topic) {
return NotificationsEnabledValue(thread->peer());
}
return rpl::merge(
topic->session().changes().topicFlagsValue(
topic,
Data::TopicUpdate::Flag::Notifications
) | rpl::to_empty,
topic->session().changes().peerUpdates(
topic->peer(),
UpdateFlag::Notifications
) | rpl::to_empty,
topic->owner().notifySettings().defaultUpdates(topic->peer())
) | rpl::map([=] {
return !topic->owner().notifySettings().isMuted(topic);
}) | rpl::distinct_until_changed();
}
rpl::producer<bool> NotificationsEnabledValue(not_null<PeerData*> peer) {
return rpl::merge(
peer->session().changes().peerFlagsValue(
peer,
UpdateFlag::Notifications
) | rpl::to_empty,
peer->owner().notifySettings().defaultUpdates(peer)
) | rpl::map([=] {
return !peer->owner().notifySettings().isMuted(peer);
}) | rpl::distinct_until_changed();
}
rpl::producer<bool> IsContactValue(not_null<UserData*> user) {
return user->session().changes().peerFlagsValue(
user,
UpdateFlag::IsContact
) | rpl::map([=] {
return user->isContact();
});
}
[[nodiscard]] rpl::producer<QString> InviteToChatButton(
not_null<UserData*> user) {
if (!user->isBot()
|| user->isRepliesChat()
|| user->isVerifyCodes()
|| user->isSupport()) {
return rpl::single(QString());
}
using Flag = Data::PeerUpdate::Flag;
return user->session().changes().peerFlagsValue(
user,
Flag::BotCanBeInvited | Flag::Rights
) | rpl::map([=] {
const auto info = user->botInfo.get();
return info->cantJoinGroups
? (info->channelAdminRights
? tr::lng_profile_invite_to_channel(tr::now)
: QString())
: (info->channelAdminRights
? tr::lng_profile_add_bot_as_admin(tr::now)
: tr::lng_profile_invite_to_group(tr::now));
});
}
[[nodiscard]] rpl::producer<QString> InviteToChatAbout(
not_null<UserData*> user) {
if (!user->isBot()
|| user->isRepliesChat()
|| user->isVerifyCodes()
|| user->isSupport()) {
return rpl::single(QString());
}
using Flag = Data::PeerUpdate::Flag;
return user->session().changes().peerFlagsValue(
user,
Flag::BotCanBeInvited | Flag::Rights
) | rpl::map([=] {
const auto info = user->botInfo.get();
return (info->cantJoinGroups || !info->groupAdminRights)
? (info->channelAdminRights
? tr::lng_profile_invite_to_channel_about(tr::now)
: QString())
: (info->channelAdminRights
? tr::lng_profile_add_bot_as_admin_about(tr::now)
: tr::lng_profile_invite_to_group_about(tr::now));
});
}
rpl::producer<bool> CanShareContactValue(not_null<UserData*> user) {
return user->session().changes().peerFlagsValue(
user,
UpdateFlag::CanShareContact
) | rpl::map([=] {
return user->canShareThisContact();
});
}
rpl::producer<bool> CanAddContactValue(not_null<UserData*> user) {
using namespace rpl::mappers;
if (user->isBot() || user->isSelf() || user->isInaccessible()) {
return rpl::single(false);
}
return IsContactValue(
user
) | rpl::map(!_1);
}
rpl::producer<Data::Birthday> BirthdayValue(not_null<UserData*> user) {
return user->session().changes().peerFlagsValue(
user,
UpdateFlag::Birthday
) | rpl::map([=] {
return user->birthday();
});
}
rpl::producer<ChannelData*> PersonalChannelValue(not_null<UserData*> user) {
return user->session().changes().peerFlagsValue(
user,
UpdateFlag::PersonalChannel
) | rpl::map([=] {
const auto channelId = user->personalChannelId();
return channelId ? user->owner().channel(channelId).get() : nullptr;
});
}
rpl::producer<bool> AmInChannelValue(not_null<ChannelData*> channel) {
return channel->session().changes().peerFlagsValue(
channel,
UpdateFlag::ChannelAmIn
) | rpl::map([=] {
return channel->amIn();
});
}
rpl::producer<int> MembersCountValue(not_null<PeerData*> peer) {
if (const auto chat = peer->asChat()) {
return peer->session().changes().peerFlagsValue(
peer,
UpdateFlag::Members
) | rpl::map([=] {
return chat->amIn()
? std::max(chat->count, int(chat->participants.size()))
: 0;
});
} else if (const auto channel = peer->asChannel()) {
return peer->session().changes().peerFlagsValue(
peer,
UpdateFlag::Members
) | rpl::map([=] {
return channel->membersCount();
});
}
Unexpected("User in MembersCountViewer().");
}
rpl::producer<int> PendingRequestsCountValue(not_null<PeerData*> peer) {
if (const auto chat = peer->asChat()) {
return peer->session().changes().peerFlagsValue(
peer,
UpdateFlag::PendingRequests
) | rpl::map([=] {
return chat->pendingRequestsCount();
});
} else if (const auto channel = peer->asChannel()) {
return peer->session().changes().peerFlagsValue(
peer,
UpdateFlag::PendingRequests
) | rpl::map([=] {
return channel->pendingRequestsCount();
});
}
Unexpected("User in MembersCountViewer().");
}
rpl::producer<int> AdminsCountValue(not_null<PeerData*> peer) {
if (const auto chat = peer->asChat()) {
return peer->session().changes().peerFlagsValue(
peer,
UpdateFlag::Admins | UpdateFlag::Rights
) | rpl::map([=] {
return chat->participants.empty()
? 0
: int(chat->admins.size() + (chat->creator ? 1 : 0));
});
} else if (const auto channel = peer->asChannel()) {
return peer->session().changes().peerFlagsValue(
peer,
UpdateFlag::Admins | UpdateFlag::Rights
) | rpl::map([=] {
return channel->canViewAdmins()
? channel->adminsCount()
: 0;
});
}
Unexpected("User in AdminsCountValue().");
}
rpl::producer<int> RestrictionsCountValue(not_null<PeerData*> peer) {
const auto countOfRestrictions = [](
Data::RestrictionsSetOptions options,
ChatRestrictions restrictions) {
auto count = 0;
const auto list = Data::ListOfRestrictions(options);
for (const auto &f : list) {
if (restrictions & f) count++;
}
return int(list.size()) - count;
};
if (const auto chat = peer->asChat()) {
return peer->session().changes().peerFlagsValue(
peer,
UpdateFlag::Rights
) | rpl::map([=] {
return countOfRestrictions({}, chat->defaultRestrictions());
});
} else if (const auto channel = peer->asChannel()) {
return rpl::combine(
Data::PeerFlagValue(channel, ChannelData::Flag::Forum),
channel->session().changes().peerFlagsValue(
channel,
UpdateFlag::Rights)
) | rpl::map([=] {
return countOfRestrictions(
{ .isForum = channel->isForum() },
channel->defaultRestrictions());
});
}
Unexpected("User in RestrictionsCountValue().");
}
rpl::producer<not_null<PeerData*>> MigratedOrMeValue(
not_null<PeerData*> peer) {
if (const auto chat = peer->asChat()) {
return peer->session().changes().peerFlagsValue(
peer,
UpdateFlag::Migration
) | rpl::map([=] {
return chat->migrateToOrMe();
});
} else {
return rpl::single(peer);
}
}
rpl::producer<int> RestrictedCountValue(not_null<ChannelData*> channel) {
return channel->session().changes().peerFlagsValue(
channel,
UpdateFlag::BannedUsers | UpdateFlag::Rights
) | rpl::map([=] {
return channel->canViewBanned()
? channel->restrictedCount()
: 0;
});
}
rpl::producer<int> KickedCountValue(not_null<ChannelData*> channel) {
return channel->session().changes().peerFlagsValue(
channel,
UpdateFlag::BannedUsers | UpdateFlag::Rights
) | rpl::map([=] {
return channel->canViewBanned()
? channel->kickedCount()
: 0;
});
}
rpl::producer<int> SharedMediaCountValue(
not_null<PeerData*> peer,
MsgId topicRootId,
PeerId monoforumPeerId,
PeerData *migrated,
Storage::SharedMediaType type) {
auto aroundId = 0;
auto limit = 0;
auto updated = SharedMediaMergedViewer(
&peer->session(),
SharedMediaMergedKey(
SparseIdsMergedSlice::Key(
peer->id,
topicRootId,
monoforumPeerId,
migrated ? migrated->id : 0,
aroundId),
type),
limit,
limit
) | rpl::map([](const SparseIdsMergedSlice &slice) {
return slice.fullCount();
}) | rpl::filter_optional();
return rpl::single(0) | rpl::then(std::move(updated));
}
rpl::producer<int> CommonGroupsCountValue(not_null<UserData*> user) {
return user->session().changes().peerFlagsValue(
user,
UpdateFlag::CommonChats
) | rpl::map([=] {
return user->commonChatsCount();
});
}
rpl::producer<int> SimilarPeersCountValue(
not_null<PeerData*> peer) {
const auto participants = &peer->session().api().chatParticipants();
participants->loadSimilarPeers(peer);
return rpl::single(peer) | rpl::then(
participants->similarLoaded()
) | rpl::filter(
rpl::mappers::_1 == peer
) | rpl::map([=] {
const auto &similar = participants->similar(peer);
return int(similar.list.size()) + similar.more;
});
}
rpl::producer<int> SavedSublistCountValue(
not_null<PeerData*> peer) {
const auto saved = &peer->owner().savedMessages();
const auto sublist = saved->sublist(peer);
if (!sublist->fullCount().has_value()) {
sublist->loadFullCount();
return rpl::single(0) | rpl::then(sublist->fullCountValue());
}
return sublist->fullCountValue();
}
rpl::producer<int> PeerGiftsCountValue(not_null<PeerData*> peer) {
return peer->session().changes().peerFlagsValue(
peer,
UpdateFlag::PeerGifts
) | rpl::map([=] {
return peer->peerGiftsCount();
});
}
rpl::producer<bool> CanAddMemberValue(not_null<PeerData*> peer) {
if (const auto chat = peer->asChat()) {
return peer->session().changes().peerFlagsValue(
peer,
UpdateFlag::Rights
) | rpl::map([=] {
return chat->canAddMembers();
});
} else if (const auto channel = peer->asChannel()) {
return peer->session().changes().peerFlagsValue(
peer,
UpdateFlag::Rights
) | rpl::map([=] {
return channel->canAddMembers();
});
}
return rpl::single(false);
}
rpl::producer<int> FullReactionsCountValue(
not_null<Main::Session*> session) {
const auto reactions = &session->data().reactions();
return rpl::single(rpl::empty) | rpl::then(
reactions->defaultUpdates()
) | rpl::map([=] {
return int(reactions->list(Data::Reactions::Type::Active).size());
}) | rpl::distinct_until_changed();
}
rpl::producer<bool> CanViewParticipantsValue(
not_null<ChannelData*> megagroup) {
if (megagroup->amCreator()) {
return rpl::single(true);
}
return rpl::combine(
megagroup->session().changes().peerFlagsValue(
megagroup,
UpdateFlag::Rights),
megagroup->flagsValue(),
[=] { return megagroup->canViewMembers(); }
) | rpl::distinct_until_changed();
}
template <typename Flag, typename Peer>
rpl::producer<BadgeType> BadgeValueFromFlags(Peer peer) {
return rpl::combine(
Data::PeerFlagsValue(
peer,
Flag::Verified | Flag::Scam | Flag::Fake),
Data::PeerPremiumValue(peer)
) | rpl::map([=](base::flags<Flag> value, bool premium) {
return (value & Flag::Scam)
? BadgeType::Scam
: (value & Flag::Fake)
? BadgeType::Fake
: peer->isMonoforum()
? BadgeType::Direct
: (value & Flag::Verified)
? BadgeType::Verified
: premium
? BadgeType::Premium
: BadgeType::None;
});
}
rpl::producer<BadgeType> BadgeValue(not_null<PeerData*> peer) {
if (const auto user = peer->asUser()) {
return BadgeValueFromFlags<UserDataFlag>(user);
} else if (const auto channel = peer->asChannel()) {
return BadgeValueFromFlags<ChannelDataFlag>(channel);
}
return rpl::single(BadgeType::None);
}
rpl::producer<EmojiStatusId> EmojiStatusIdValue(not_null<PeerData*> peer) {
if (peer->isChat()) {
return rpl::single(EmojiStatusId());
}
return peer->session().changes().peerFlagsValue(
peer,
Data::PeerUpdate::Flag::EmojiStatus
) | rpl::map([=] { return peer->emojiStatusId(); });
}
rpl::producer<QString> BirthdayLabelText(
rpl::producer<Data::Birthday> birthday) {
return std::move(birthday) | rpl::map([](Data::Birthday value) {
return rpl::conditional(
Data::IsBirthdayTodayValue(value),
tr::lng_info_birthday_today_label(),
tr::lng_info_birthday_label());
}) | rpl::flatten_latest();
}
rpl::producer<QString> BirthdayValueText(
rpl::producer<Data::Birthday> birthday) {
return std::move(
birthday
) | rpl::map([](Data::Birthday value) -> rpl::producer<QString> {
if (!value) {
return rpl::single(QString());
}
return Data::IsBirthdayTodayValue(
value
) | rpl::map([=](bool today) {
auto text = Data::BirthdayText(value);
if (const auto age = Data::BirthdayAge(value)) {
text = (today
? tr::lng_info_birthday_today_years
: tr::lng_info_birthday_years)(
tr::now,
lt_count,
age,
lt_date,
text);
}
if (today) {
text = tr::lng_info_birthday_today(
tr::now,
lt_emoji,
Data::BirthdayCake(),
lt_date,
text);
}
return text;
});
}) | rpl::flatten_latest();
}
} // namespace Profile
} // namespace Info

View File

@@ -0,0 +1,148 @@
/*
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 "base/flags.h"
#include <rpl/producer.h>
#include <rpl/map.h>
struct ChannelLocation;
namespace Data {
class ForumTopic;
class Thread;
class Birthday;
} // namespace Data
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class RpWidget;
template <typename Widget>
class SlideWrap;
} // namespace Ui
namespace Storage {
enum class SharedMediaType : signed char;
} // namespace Storage
namespace Info::Profile {
inline auto ToSingleLine() {
return rpl::map([](const QString &text) {
return TextUtilities::SingleLine(text);
});
}
rpl::producer<not_null<PeerData*>> MigratedOrMeValue(
not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<QString> NameValue(not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<QString> TitleValue(
not_null<Data::ForumTopic*> topic);
[[nodiscard]] rpl::producer<DocumentId> IconIdValue(
not_null<Data::ForumTopic*> topic);
[[nodiscard]] rpl::producer<int32> ColorIdValue(
not_null<Data::ForumTopic*> topic);
[[nodiscard]] rpl::producer<TextWithEntities> PhoneValue(
not_null<UserData*> user);
[[nodiscard]] rpl::producer<TextWithEntities> PhoneOrHiddenValue(
not_null<UserData*> user);
[[nodiscard]] rpl::producer<TextWithEntities> UsernameValue(
not_null<PeerData*> peer,
bool primary = false);
[[nodiscard]] rpl::producer<std::vector<TextWithEntities>> UsernamesValue(
not_null<PeerData*> peer);
[[nodiscard]] QString UsernameUrl(
not_null<PeerData*> peer,
const QString &username,
bool link = false);
[[nodiscard]] TextWithEntities AboutWithEntities(
not_null<PeerData*> peer,
const QString &value);
[[nodiscard]] rpl::producer<TextWithEntities> AboutValue(
not_null<PeerData*> peer);
struct LinkWithUrl {
QString text;
QString url;
};
[[nodiscard]] QString TopicLink(
not_null<Data::ForumTopic*> topic,
bool full);
[[nodiscard]] rpl::producer<LinkWithUrl> LinkValue(
not_null<PeerData*> peer,
bool primary = false,
MsgId topicRootId = 0);
[[nodiscard]] rpl::producer<const ChannelLocation*> LocationValue(
not_null<ChannelData*> channel);
[[nodiscard]] rpl::producer<bool> NotificationsEnabledValue(
not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<bool> NotificationsEnabledValue(
not_null<Data::Thread*> thread);
[[nodiscard]] rpl::producer<bool> IsContactValue(not_null<UserData*> user);
[[nodiscard]] rpl::producer<QString> InviteToChatButton(
not_null<UserData*> user);
[[nodiscard]] rpl::producer<QString> InviteToChatAbout(
not_null<UserData*> user);
[[nodiscard]] rpl::producer<bool> CanShareContactValue(
not_null<UserData*> user);
[[nodiscard]] rpl::producer<bool> CanAddContactValue(
not_null<UserData*> user);
[[nodiscard]] rpl::producer<Data::Birthday> BirthdayValue(
not_null<UserData*> user);
[[nodiscard]] rpl::producer<ChannelData*> PersonalChannelValue(
not_null<UserData*> user);
[[nodiscard]] rpl::producer<bool> AmInChannelValue(
not_null<ChannelData*> channel);
[[nodiscard]] rpl::producer<int> MembersCountValue(not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<int> PendingRequestsCountValue(
not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<int> AdminsCountValue(not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<int> RestrictionsCountValue(
not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<int> RestrictedCountValue(
not_null<ChannelData*> channel);
[[nodiscard]] rpl::producer<int> KickedCountValue(
not_null<ChannelData*> channel);
[[nodiscard]] rpl::producer<int> SharedMediaCountValue(
not_null<PeerData*> peer,
MsgId topicRootId,
PeerId monoforumPeerId,
PeerData *migrated,
Storage::SharedMediaType type);
[[nodiscard]] rpl::producer<int> CommonGroupsCountValue(
not_null<UserData*> user);
[[nodiscard]] rpl::producer<int> SimilarPeersCountValue(
not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<int> SavedSublistCountValue(
not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<int> PeerGiftsCountValue(
not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<bool> CanAddMemberValue(
not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<int> FullReactionsCountValue(
not_null<Main::Session*> peer);
[[nodiscard]] rpl::producer<bool> CanViewParticipantsValue(
not_null<ChannelData*> megagroup);
enum class BadgeType : uchar;
[[nodiscard]] rpl::producer<BadgeType> BadgeValue(not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<EmojiStatusId> EmojiStatusIdValue(
not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<QString> BirthdayLabelText(
rpl::producer<Data::Birthday> birthday);
[[nodiscard]] rpl::producer<QString> BirthdayValueText(
rpl::producer<Data::Birthday> birthday);
} // namespace Info::Profile

View File

@@ -0,0 +1,252 @@
/*
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 "info/profile/info_profile_widget.h"
#include "dialogs/ui/dialogs_stories_content.h"
#include "history/history.h"
#include "info/profile/info_profile_inner_widget.h"
#include "info/profile/info_profile_members.h"
#include "info/settings/info_settings_widget.h"
#include "ui/widgets/scroll_area.h"
#include "ui/ui_utility.h"
#include "data/data_peer.h"
#include "data/data_channel.h"
#include "data/data_forum_topic.h"
#include "data/data_saved_sublist.h"
#include "data/data_user.h"
#include "lang/lang_keys.h"
#include "info/info_controller.h"
#include "styles/style_info.h"
#include <QtWidgets/QApplication>
#include <QtWidgets/QScrollBar>
namespace Info::Settings {
struct SectionCustomTopBarData;
} // namespace Info::Settings
namespace Info::Profile {
using Info::Settings::SectionCustomTopBarData;
Memento::Memento(not_null<Controller*> controller)
: Memento(
controller->peer(),
controller->topic(),
controller->sublist(),
controller->migratedPeerId(),
{ v::null }) {
}
Memento::Memento(
not_null<PeerData*> peer,
PeerId migratedPeerId,
Origin origin)
: Memento(peer, nullptr, nullptr, migratedPeerId, origin) {
}
Memento::Memento(
not_null<PeerData*> peer,
Data::ForumTopic *topic,
Data::SavedSublist *sublist,
PeerId migratedPeerId,
Origin origin)
: ContentMemento(peer, topic, sublist, migratedPeerId)
, _origin(origin) {
}
Memento::Memento(not_null<Data::ForumTopic*> topic)
: ContentMemento(topic->peer(), topic, nullptr, 0) {
}
Memento::Memento(not_null<Data::SavedSublist*> sublist)
: ContentMemento(sublist->owningHistory()->peer, nullptr, sublist, 0) {
}
Section Memento::section() const {
return Section(Section::Type::Profile);
}
object_ptr<ContentWidget> Memento::createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) {
auto result = object_ptr<Widget>(parent, controller, _origin);
result->setInternalState(geometry, this);
return result;
}
void Memento::setMembersState(std::unique_ptr<MembersState> state) {
_membersState = std::move(state);
}
std::unique_ptr<MembersState> Memento::membersState() {
return std::move(_membersState);
}
Memento::~Memento() = default;
Widget::Widget(
QWidget *parent,
not_null<Controller*> controller,
Origin origin)
: ContentWidget(parent, controller)
, _inner(
setupFlexibleInnerWidget(
object_ptr<InnerWidget>(this, controller, origin),
_flexibleScroll))
, _pinnedToTop(_inner->createPinnedToTop(this))
, _pinnedToBottom(_inner->createPinnedToBottom(this)) {
controller->setSearchEnabledByContent(false);
_inner->move(0, 0);
_inner->scrollToRequests(
) | rpl::on_next([this](Ui::ScrollToRequest request) {
if (request.ymin < 0) {
scrollTopRestore(
qMin(scrollTopSave(), request.ymax));
} else {
scrollTo(request);
}
}, lifetime());
_inner->backRequest() | rpl::on_next([=] {
checkBeforeClose([=] { controller->showBackFromStack(); });
}, _inner->lifetime());
if (_pinnedToTop) {
_inner->widthValue(
) | rpl::on_next([=](int w) {
_pinnedToTop->resizeToWidth(w);
setScrollTopSkip(_pinnedToTop->height());
}, _pinnedToTop->lifetime());
_pinnedToTop->heightValue(
) | rpl::on_next([=](int h) {
setScrollTopSkip(h);
}, _pinnedToTop->lifetime());
}
if (_pinnedToBottom) {
const auto processHeight = [=] {
setScrollBottomSkip(_pinnedToBottom->height());
_pinnedToBottom->moveToLeft(
_pinnedToBottom->x(),
height() - _pinnedToBottom->height());
};
_inner->sizeValue(
) | rpl::on_next([=](const QSize &s) {
_pinnedToBottom->resizeToWidth(s.width());
}, _pinnedToBottom->lifetime());
rpl::combine(
_pinnedToBottom->heightValue(),
heightValue()
) | rpl::on_next(processHeight, _pinnedToBottom->lifetime());
}
if (_pinnedToTop
&& _pinnedToTop->minimumHeight()
&& _inner->hasFlexibleTopBar()) {
_flexibleScrollHelper = std::make_unique<FlexibleScrollHelper>(
scroll(),
_inner,
_pinnedToTop.get(),
[=](QMargins margins) {
ContentWidget::setPaintPadding(std::move(margins));
},
[=](rpl::producer<not_null<QEvent*>> &&events) {
ContentWidget::setViewport(std::move(events));
},
_flexibleScroll);
}
}
void Widget::setInnerFocus() {
_inner->setFocus();
}
void Widget::enableBackButton() {
_inner->enableBackButton();
}
void Widget::showFinished() {
_inner->showFinished();
}
rpl::producer<QString> Widget::title() {
if (const auto topic = controller()->key().topic()) {
return topic->peer()->isBot()
? tr::lng_info_thread_title()
: tr::lng_info_topic_title();
} else if (controller()->key().sublist()
&& controller()->key().sublist()->parentChat()) {
return tr::lng_profile_direct_messages();
}
const auto peer = controller()->key().peer();
if (const auto user = peer->asUser()) {
return (user->isBot() && !user->isSupport())
? tr::lng_info_bot_title()
: tr::lng_info_user_title();
} else if (const auto channel = peer->asChannel()) {
return channel->isMonoforum()
? tr::lng_profile_direct_messages()
: channel->isMegagroup()
? tr::lng_info_group_title()
: tr::lng_info_channel_title();
} else if (peer->isChat()) {
return tr::lng_info_group_title();
}
Unexpected("Bad peer type in Info::TitleValue()");
}
rpl::producer<Dialogs::Stories::Content> Widget::titleStories() {
const auto peer = controller()->key().peer();
if (peer && !peer->isChat()) {
return Dialogs::Stories::LastForPeer(peer);
}
return nullptr;
}
bool Widget::showInternal(not_null<ContentMemento*> memento) {
if (!controller()->validateMementoPeer(memento)) {
return false;
}
if (auto profileMemento = dynamic_cast<Memento*>(memento.get())) {
restoreState(profileMemento);
return true;
}
return false;
}
void Widget::setInternalState(
const QRect &geometry,
not_null<Memento*> memento) {
setGeometry(geometry);
Ui::SendPendingMoveResizeEvents(this);
restoreState(memento);
}
std::shared_ptr<ContentMemento> Widget::doCreateMemento() {
auto result = std::make_shared<Memento>(controller());
saveState(result.get());
return result;
}
void Widget::saveState(not_null<Memento*> memento) {
memento->setScrollTop(scrollTopSave());
_inner->saveState(memento);
}
void Widget::restoreState(not_null<Memento*> memento) {
_inner->restoreState(memento);
scrollTopRestore(memento->scrollTop());
}
} // namespace Info::Profile

View File

@@ -0,0 +1,102 @@
/*
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 "info/info_content_widget.h"
#include "ui/effects/animations.h"
namespace Data {
class ForumTopic;
} // namespace Data
namespace Info::Profile {
class InnerWidget;
struct MembersState;
struct GroupReactionOrigin {
not_null<PeerData*> group;
MsgId messageId = 0;
};
struct Origin {
std::variant<v::null_t, GroupReactionOrigin> data;
};
class Memento final : public ContentMemento {
public:
explicit Memento(not_null<Controller*> controller);
Memento(
not_null<PeerData*> peer,
PeerId migratedPeerId,
Origin origin = { v::null });
explicit Memento(not_null<Data::ForumTopic*> topic);
explicit Memento(not_null<Data::SavedSublist*> sublist);
object_ptr<ContentWidget> createWidget(
QWidget *parent,
not_null<Controller*> controller,
const QRect &geometry) override;
Section section() const override;
[[nodiscard]] Origin origin() const {
return _origin;
}
void setMembersState(std::unique_ptr<MembersState> state);
std::unique_ptr<MembersState> membersState();
~Memento();
private:
Memento(
not_null<PeerData*> peer,
Data::ForumTopic *topic,
Data::SavedSublist *sublist,
PeerId migratedPeerId,
Origin origin);
std::unique_ptr<MembersState> _membersState;
Origin _origin;
};
class Widget final : public ContentWidget {
public:
Widget(QWidget *parent, not_null<Controller*> controller, Origin origin);
bool showInternal(
not_null<ContentMemento*> memento) override;
void setInternalState(
const QRect &geometry,
not_null<Memento*> memento);
void setInnerFocus() override;
void enableBackButton() override;
void showFinished() override;
rpl::producer<QString> title() override;
rpl::producer<Dialogs::Stories::Content> titleStories() override;
private:
void saveState(not_null<Memento*> memento);
void restoreState(not_null<Memento*> memento);
std::shared_ptr<ContentMemento> doCreateMemento() override;
FlexibleScrollData _flexibleScroll;
InnerWidget *_inner = nullptr;
base::weak_qptr<Ui::RpWidget> _pinnedToTop;
base::weak_qptr<Ui::RpWidget> _pinnedToBottom;
std::unique_ptr<FlexibleScrollHelper> _flexibleScrollHelper;
};
} // namespace Info::Profile