Show common groups userpics in new-peer-info.

This commit is contained in:
John Preston 2025-03-07 17:25:06 +04:00
parent c9fb97cd7c
commit ab58e7a225
9 changed files with 304 additions and 13 deletions

View file

@ -69,6 +69,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_domain.h"
#include "main/main_session.h"
#include "main/main_session_settings.h"
#include "info/info_controller.h"
#include "info/info_memento.h"
#include "inline_bots/bot_attach_web_view.h"
#include "history/history.h"
#include "history/history_item.h"
@ -1034,6 +1036,26 @@ bool EditPaidMessagesFee(
return true;
}
bool ShowCommonGroups(
Window::SessionController *controller,
const Match &match,
const QVariant &context) {
if (!controller) {
return false;
}
const auto peerId = PeerId(match->captured(1).toULongLong());
if (const auto id = peerToUser(peerId)) {
const auto user = controller->session().data().userLoaded(id);
if (user) {
controller->showSection(
std::make_shared<Info::Memento>(
user,
Info::Section::Type::CommonGroups));
}
}
return true;
}
bool ShowStarsExamples(
Window::SessionController *controller,
const Match &match,
@ -1541,6 +1563,10 @@ const std::vector<LocalUrlHandler> &InternalUrlHandlers() {
u"^edit_paid_messages_fee/([0-9]+)$"_q,
EditPaidMessagesFee,
},
{
u"^common_groups/([0-9]+)$"_q,
ShowCommonGroups,
},
{
u"^stars_examples$"_q,
ShowStarsExamples,

View file

@ -4342,6 +4342,9 @@ void HistoryInner::refreshAboutView(bool force) {
_aboutView = std::make_unique<HistoryView::AboutView>(
_history,
_history->delegateMixin()->delegate());
_aboutView->refreshRequests() | rpl::start_with_next([=] {
updateBotInfo();
}, _aboutView->lifetime());
}
};
if (const auto user = _peer->asUser()) {

View file

@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_document.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "history/view/history_view_group_call_bar.h"
#include "history/view/media/history_view_media_generic.h"
#include "history/view/media/history_view_service_box.h"
#include "history/view/media/history_view_sticker_player_abstract.h"
@ -37,17 +38,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "settings/settings_credits.h" // BuyStarsHandler
#include "settings/settings_premium.h"
#include "ui/chat/chat_style.h"
#include "ui/text/custom_emoji_instance.h"
#include "ui/text/text_utilities.h"
#include "ui/text/text_options.h"
#include "ui/dynamic_image.h"
#include "ui/painter.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h" // GroupCallUserpics
#include "styles/style_credits.h"
namespace HistoryView {
namespace {
constexpr auto kLabelOpacity = 0.85;
constexpr auto kMaxCommonChatsUserpics = 3;
class EmptyChatLockedBox final
: public ServiceBoxContent
@ -95,6 +100,101 @@ private:
};
class UserpicsList final : public Ui::DynamicImage {
public:
UserpicsList(
std::vector<not_null<PeerData*>> peers,
const style::GroupCallUserpics &st,
int countOverride = 0);
[[nodiscard]] int width() const;
std::shared_ptr<Ui::DynamicImage> clone() override;
QImage image(int size) override;
void subscribeToUpdates(Fn<void()> callback) override;
private:
struct Subscribed {
explicit Subscribed(Fn<void()> callback)
: callback(std::move(callback)) {
}
std::vector<HistoryView::UserpicInRow> list;
bool someNotLoaded = false;
Fn<void()> callback;
int paletteVersion = 0;
};
const std::vector<not_null<PeerData*>> _peers;
const style::GroupCallUserpics &_st;
const int _countOverride = 0;
QImage _frame;
std::unique_ptr<Subscribed> _subscribed;
};
UserpicsList::UserpicsList(
std::vector<not_null<PeerData*>> peers,
const style::GroupCallUserpics &st,
int countOverride)
: _peers(std::move(peers))
, _st(st)
, _countOverride(countOverride) {
}
std::shared_ptr<Ui::DynamicImage> UserpicsList::clone() {
return std::make_shared<UserpicsList>(_peers, _st);
}
QImage UserpicsList::image(int size) {
Expects(_subscribed != nullptr);
const auto regenerate = [&] {
const auto version = style::PaletteVersion();
if (_frame.isNull() || _subscribed->paletteVersion != version) {
_subscribed->paletteVersion = version;
return true;
}
for (auto &entry : _subscribed->list) {
const auto peer = entry.peer;
auto &view = entry.view;
const auto wasView = view.cloud.get();
if (peer->userpicUniqueKey(view) != entry.uniqueKey
|| view.cloud.get() != wasView) {
return true;
}
}
return false;
}();
if (regenerate) {
const auto max = std::max(_countOverride, int(_peers.size()));
GenerateUserpicsInRow(_frame, _subscribed->list, _st, max);
}
return _frame;
}
void UserpicsList::subscribeToUpdates(Fn<void()> callback) {
if (!callback) {
_subscribed = nullptr;
return;
}
_subscribed = std::make_unique<Subscribed>(std::move(callback));
for (const auto peer : _peers) {
_subscribed->list.push_back({ .peer = peer });
}
}
int UserpicsList::width() const {
const auto count = std::max(_countOverride, int(_peers.size()));
if (!count) {
return 0;
}
const auto shifted = count - 1;
return _st.size + (shifted * (_st.size - _st.shift));
}
auto GenerateChatIntro(
not_null<Element*> parent,
Element *replacing,
@ -165,7 +265,8 @@ auto GenerateChatIntro(
auto GenerateNewPeerInfo(
not_null<Element*> parent,
Element *replacing,
not_null<UserData*> user)
not_null<UserData*> user,
std::vector<not_null<PeerData*>> commonGroups)
-> Fn<void(
not_null<MediaGeneric*>,
Fn<void(std::unique_ptr<MediaGenericPart>)>)> {
@ -208,9 +309,22 @@ auto GenerateNewPeerInfo(
});
}
const auto context = Core::TextContext({
.session = &parent->history()->session(),
.repaint = [parent] { parent->repaint(); },
});
const auto kUserpicsPrefix = u"userpics-list/"_q;
if (const auto count = user->commonChatsCount()) {
const auto url = u"internal:common_groups/"_q
+ QString::number(user->id.value);
auto ids = QStringList();
const auto userpics = std::min(count, kMaxCommonChatsUserpics);
for (auto i = 0; i != userpics; ++i) {
ids.push_back(QString::number(i < commonGroups.size()
? commonGroups[i]->id.value
: 0));
}
auto userpicsData = kUserpicsPrefix + ids.join(',');
entries.push_back({
tr::lng_new_contact_common_groups(tr::now),
Ui::Text::Wrapped(
@ -219,7 +333,7 @@ auto GenerateNewPeerInfo(
lt_count,
count,
lt_emoji,
TextWithEntities(),
Ui::Text::SingleCustomEmoji(userpicsData),
lt_arrow,
Ui::Text::IconEmoji(&st::textMoreIconEmoji),
Ui::Text::Bold),
@ -228,16 +342,40 @@ auto GenerateNewPeerInfo(
});
}
auto copy = context;
copy.customEmojiFactory = [=, old = copy.customEmojiFactory](
QStringView data,
const Ui::Text::MarkedContext &context
) -> std::unique_ptr<Ui::Text::CustomEmoji> {
if (!data.startsWith(kUserpicsPrefix)) {
return old(data, context);
}
const auto ids = data.mid(kUserpicsPrefix.size()).split(',');
auto peers = std::vector<not_null<PeerData*>>();
for (const auto &id : ids) {
if (const auto peerId = PeerId(id.toULongLong())) {
peers.push_back(user->owner().peer(peerId));
}
}
auto image = std::make_shared<UserpicsList>(
std::move(peers),
st::newPeerUserpics,
ids.size());
const auto size = image->width();
return std::make_unique<Ui::CustomEmoji::DynamicImageEmoji>(
data.toString(),
std::move(image),
context.repaint,
st::newPeerUserpicsPadding,
size);
};
push(std::make_unique<AttributeTable>(
std::move(entries),
st::newPeerSubtitleMargin,
fadedFg,
normalFg));
normalFg,
copy));
const auto context = Core::TextContext({
.session = &parent->history()->session(),
.repaint = [parent] { parent->repaint(); },
});
const auto details = user->botVerifyDetails();
const auto text = details
? Data::SingleCustomEmoji(
@ -380,9 +518,10 @@ bool AboutView::refresh() {
if (user
&& !user->isContact()
&& !user->phoneCountryCode().isEmpty()) {
if (_item) {
if (_item && !_commonGroupsStale) {
return false;
}
loadCommonGroups();
setItem(makeNewPeerInfo(user), nullptr);
return true;
} else if (user && !user->isSelf() && _history->isDisplayedEmpty()) {
@ -474,6 +613,14 @@ void AboutView::make(Data::ChatIntro data, bool preview) {
setItem(std::move(owned), data.sticker);
}
rpl::producer<> AboutView::refreshRequests() const {
return _refreshRequests.events();
}
rpl::lifetime &AboutView::lifetime() {
return _lifetime;
}
void AboutView::toggleStickerRegistered(bool registered) {
if (const auto item = _item ? _item->data().get() : nullptr) {
if (_sticker) {
@ -490,6 +637,75 @@ void AboutView::toggleStickerRegistered(bool registered) {
}
}
void AboutView::loadCommonGroups() {
if (_commonGroupsRequested) {
return;
}
_commonGroupsRequested = true;
const auto user = _history->peer->asUser();
if (!user) {
return;
}
struct Cached {
std::vector<not_null<PeerData*>> list;
};
struct Session {
base::flat_map<not_null<UserData*>, Cached> data;
};
static auto Map = base::flat_map<not_null<Main::Session*>, Session>();
const auto session = &_history->session();
auto i = Map.find(session);
if (i == end(Map)) {
i = Map.emplace(session).first;
session->lifetime().add([session] {
Map.remove(session);
});
}
auto &cached = i->second.data[user];
const auto count = user->commonChatsCount();
if (!count) {
cached = {};
return;
} else while (cached.list.size() > count) {
cached.list.pop_back();
}
_commonGroups = cached.list;
const auto requestId = _history->session().api().request(
MTPmessages_GetCommonChats(
user->inputUser,
MTP_long(0),
MTP_int(kMaxCommonChatsUserpics))
).done([=](const MTPmessages_Chats &result) {
const auto chats = result.match([](const auto &data) {
return &data.vchats().v;
});
auto &owner = user->session().data();
auto list = std::vector<not_null<PeerData*>>();
list.reserve(chats->size());
for (const auto &chat : *chats) {
if (const auto peer = owner.processChat(chat)) {
list.push_back(peer);
if (list.size() == kMaxCommonChatsUserpics) {
break;
}
}
}
if (_commonGroups != list) {
Map[session].data[user].list = list;
_commonGroups = std::move(list);
_commonGroupsStale = true;
_refreshRequests.fire({});
}
}).send();
_lifetime.add([=] {
_history->session().api().request(requestId).cancel();
});
}
void AboutView::setHelloChosen(not_null<DocumentData*> sticker) {
_helloChosen = sticker;
toggleStickerRegistered(false);
@ -505,6 +721,8 @@ void AboutView::setItem(AdminLog::OwnedItem item, DocumentData *sticker) {
}
AdminLog::OwnedItem AboutView::makeNewPeerInfo(not_null<UserData*> user) {
_commonGroupsStale = false;
const auto text = user->name();
const auto item = _history->makeMessage({
.id = _history->nextNonHistoryEntryId(),
@ -517,7 +735,7 @@ AdminLog::OwnedItem AboutView::makeNewPeerInfo(not_null<UserData*> user) {
auto owned = AdminLog::OwnedItem(_delegate, item);
owned->overrideMedia(std::make_unique<HistoryView::MediaGeneric>(
owned.get(),
GenerateNewPeerInfo(owned.get(), _item.get(), user),
GenerateNewPeerInfo(owned.get(), _item.get(), user, _commonGroups),
HistoryView::MediaGenericDescriptor{
.service = true,
.hideServiceText = true,

View file

@ -30,6 +30,9 @@ public:
void make(Data::ChatIntro data, bool preview = false);
[[nodiscard]] rpl::producer<> refreshRequests() const;
[[nodiscard]] rpl::lifetime &lifetime();
int top = 0;
int height = 0;
@ -50,13 +53,22 @@ private:
void setHelloChosen(not_null<DocumentData*> sticker);
void toggleStickerRegistered(bool registered);
void loadCommonGroups();
const not_null<History*> _history;
const not_null<ElementDelegate*> _delegate;
AdminLog::OwnedItem _item;
DocumentData *_helloChosen = nullptr;
DocumentData *_sticker = nullptr;
int _version = 0;
bool _commonGroupsStale = false;
bool _commonGroupsRequested = false;
std::vector<not_null<PeerData*>> _commonGroups;
rpl::event_stream<> _refreshRequests;
rpl::lifetime _lifetime;
};
} // namespace HistoryView

View file

@ -511,6 +511,26 @@ void AttributeTable::draw(
}
}
TextState AttributeTable::textState(
QPoint point,
StateRequest request,
int outerWidth) const {
auto top = _margins.top();
for (const auto &part : _parts) {
const auto height = st::normalFont->height + st::chatUniqueRowSkip;
if (point.y() >= top && point.y() < top + height) {
point -= QPoint((outerWidth - width()) / 2 + _valueLeft, top);
auto result = TextState();
auto forText = request.forText();
forText.align = style::al_topleft;
result.link = part.value.getState(point, width(), forText).link;
return result;
}
top += height;
}
return {};
}
QSize AttributeTable::countOptimalSize() {
auto maxLabel = 0;
auto maxValue = 0;

View file

@ -97,6 +97,10 @@ public:
not_null<const MediaGeneric*> owner,
const PaintContext &context,
int outerWidth) const override;
TextState textState(
QPoint point,
StateRequest request,
int outerWidth) const override;
QSize countOptimalSize() override;
QSize countCurrentSize(int newWidth) override;

View file

@ -1237,3 +1237,10 @@ newPeerNonOfficial: IconEmoji {
icon: icon{{ "chat/mini_info_alert", windowFg }};
padding: margins(0px, 2px, 0px, 0px);
}
newPeerUserpics: GroupCallUserpics {
size: 16px;
shift: 5px;
stroke: 1px;
align: align(left);
}
newPeerUserpicsPadding: margins(0px, 3px, 0px, 0px);

View file

@ -296,9 +296,10 @@ void PeerBadge::set(
_botVerifiedData = std::make_unique<BotVerifiedData>();
}
if (details->iconId) {
_botVerifiedData->icon = factory(
Data::SerializeCustomEmojiId(details->iconId),
{ .repaint = repaint });
_botVerifiedData->icon = std::make_unique<Ui::Text::FirstFrameEmoji>(
factory(
Data::SerializeCustomEmojiId(details->iconId),
{ .repaint = repaint }));
}
}

@ -1 +1 @@
Subproject commit e51fe382bda9f2944412078da70f04de5dd821f3
Subproject commit b9ef54ad5eb6932930244f30a00ac9169cd09a46