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
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:
81
Telegram/SourceFiles/info/profile/info_levels.style
Normal file
81
Telegram/SourceFiles/info/profile/info_levels.style
Normal 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 }};
|
||||
3148
Telegram/SourceFiles/info/profile/info_profile_actions.cpp
Normal file
3148
Telegram/SourceFiles/info/profile/info_profile_actions.cpp
Normal file
File diff suppressed because it is too large
Load Diff
81
Telegram/SourceFiles/info/profile/info_profile_actions.h
Normal file
81
Telegram/SourceFiles/info/profile/info_profile_actions.h
Normal 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> ÷rOverridden);
|
||||
|
||||
} // namespace Info::Profile
|
||||
|
||||
299
Telegram/SourceFiles/info/profile/info_profile_badge.cpp
Normal file
299
Telegram/SourceFiles/info/profile/info_profile_badge.cpp
Normal 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
|
||||
107
Telegram/SourceFiles/info/profile/info_profile_badge.h
Normal file
107
Telegram/SourceFiles/info/profile/info_profile_badge.h
Normal 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
|
||||
224
Telegram/SourceFiles/info/profile/info_profile_badge_tooltip.cpp
Normal file
224
Telegram/SourceFiles/info/profile/info_profile_badge_tooltip.cpp
Normal 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
|
||||
@@ -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
|
||||
851
Telegram/SourceFiles/info/profile/info_profile_cover.cpp
Normal file
851
Telegram/SourceFiles/info/profile/info_profile_cover.cpp
Normal 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
|
||||
187
Telegram/SourceFiles/info/profile/info_profile_cover.h
Normal file
187
Telegram/SourceFiles/info/profile/info_profile_cover.h
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
40
Telegram/SourceFiles/info/profile/info_profile_icon.cpp
Normal file
40
Telegram/SourceFiles/info/profile/info_profile_icon.cpp
Normal 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
|
||||
40
Telegram/SourceFiles/info/profile/info_profile_icon.h
Normal file
40
Telegram/SourceFiles/info/profile/info_profile_icon.h
Normal 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
|
||||
471
Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp
Normal file
471
Telegram/SourceFiles/info/profile/info_profile_inner_widget.cpp
Normal 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
|
||||
124
Telegram/SourceFiles/info/profile/info_profile_inner_widget.h
Normal file
124
Telegram/SourceFiles/info/profile/info_profile_inner_widget.h
Normal 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
|
||||
472
Telegram/SourceFiles/info/profile/info_profile_members.cpp
Normal file
472
Telegram/SourceFiles/info/profile/info_profile_members.cpp
Normal 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
|
||||
|
||||
139
Telegram/SourceFiles/info/profile/info_profile_members.h
Normal file
139
Telegram/SourceFiles/info/profile/info_profile_members.h
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
142
Telegram/SourceFiles/info/profile/info_profile_music_button.cpp
Normal file
142
Telegram/SourceFiles/info/profile/info_profile_music_button.cpp
Normal 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
|
||||
@@ -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
|
||||
146
Telegram/SourceFiles/info/profile/info_profile_phone_menu.cpp
Normal file
146
Telegram/SourceFiles/info/profile/info_profile_phone_menu.cpp
Normal 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
|
||||
24
Telegram/SourceFiles/info/profile/info_profile_phone_menu.h
Normal file
24
Telegram/SourceFiles/info/profile/info_profile_phone_menu.h
Normal 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
|
||||
173
Telegram/SourceFiles/info/profile/info_profile_status_label.cpp
Normal file
173
Telegram/SourceFiles/info/profile/info_profile_status_label.cpp
Normal 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
|
||||
@@ -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
|
||||
69
Telegram/SourceFiles/info/profile/info_profile_text.cpp
Normal file
69
Telegram/SourceFiles/info/profile/info_profile_text.cpp
Normal 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
|
||||
43
Telegram/SourceFiles/info/profile/info_profile_text.h
Normal file
43
Telegram/SourceFiles/info/profile/info_profile_text.h
Normal 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
|
||||
2741
Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp
Normal file
2741
Telegram/SourceFiles/info/profile/info_profile_top_bar.cpp
Normal file
File diff suppressed because it is too large
Load Diff
299
Telegram/SourceFiles/info/profile/info_profile_top_bar.h
Normal file
299
Telegram/SourceFiles/info/profile/info_profile_top_bar.h
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
773
Telegram/SourceFiles/info/profile/info_profile_values.cpp
Normal file
773
Telegram/SourceFiles/info/profile/info_profile_values.cpp
Normal 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
|
||||
148
Telegram/SourceFiles/info/profile/info_profile_values.h
Normal file
148
Telegram/SourceFiles/info/profile/info_profile_values.h
Normal 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
|
||||
252
Telegram/SourceFiles/info/profile/info_profile_widget.cpp
Normal file
252
Telegram/SourceFiles/info/profile/info_profile_widget.cpp
Normal 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
|
||||
102
Telegram/SourceFiles/info/profile/info_profile_widget.h
Normal file
102
Telegram/SourceFiles/info/profile/info_profile_widget.h
Normal 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
|
||||
Reference in New Issue
Block a user