From 61ac7e6c1db1cc8b40f6e409030dd7337249c1df Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 18 Oct 2021 17:21:49 +0400 Subject: [PATCH] Start PeerShortInfoBox for small in-box profiles. --- Telegram/CMakeLists.txt | 4 + .../boxes/peers/edit_peer_requests_box.cpp | 5 +- .../boxes/peers/peer_short_info_box.cpp | 94 ++++++ .../boxes/peers/peer_short_info_box.h | 74 +++++ .../boxes/peers/prepare_short_info_box.cpp | 301 ++++++++++++++++++ .../boxes/peers/prepare_short_info_box.h | 28 ++ Telegram/SourceFiles/info/info.style | 6 + .../info/profile/info_profile_values.cpp | 24 +- .../info/profile/info_profile_values.h | 3 + 9 files changed, 529 insertions(+), 10 deletions(-) create mode 100644 Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp create mode 100644 Telegram/SourceFiles/boxes/peers/peer_short_info_box.h create mode 100644 Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp create mode 100644 Telegram/SourceFiles/boxes/peers/prepare_short_info_box.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 61ba859ffd..ae5b31d880 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -182,6 +182,10 @@ PRIVATE boxes/peers/edit_peer_requests_box.h boxes/peers/edit_peer_type_box.cpp boxes/peers/edit_peer_type_box.h + boxes/peers/peer_short_info_box.cpp + boxes/peers/peer_short_info_box.h + boxes/peers/prepare_short_info_box.cpp + boxes/peers/prepare_short_info_box.h boxes/about_box.cpp boxes/about_box.h boxes/about_sponsored_box.cpp diff --git a/Telegram/SourceFiles/boxes/peers/edit_peer_requests_box.cpp b/Telegram/SourceFiles/boxes/peers/edit_peer_requests_box.cpp index 4c37fd4f1d..f683b94140 100644 --- a/Telegram/SourceFiles/boxes/peers/edit_peer_requests_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/edit_peer_requests_box.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peer_list_controllers.h" #include "boxes/peers/edit_participants_box.h" // SubscribeToMigration #include "boxes/peers/edit_peer_invite_link.h" // PrepareRequestedRowStatus +#include "boxes/peers/prepare_short_info_box.h" // PrepareShortInfoBox #include "history/view/history_view_requests_bar.h" // kRecentRequestsLimit #include "data/data_peer.h" #include "data/data_user.h" @@ -354,7 +355,9 @@ void RequestsBoxController::refreshDescription() { } void RequestsBoxController::rowClicked(not_null row) { - _navigation->showPeerInfo(row->peer()); + _navigation->parentController()->show(PrepareShortInfoBox( + row->peer(), + _navigation)); } void RequestsBoxController::rowElementClicked( diff --git a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp new file mode 100644 index 0000000000..4b1ff59e5c --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp @@ -0,0 +1,94 @@ +/* +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 "boxes/peers/peer_short_info_box.h" + +#include "ui/widgets/labels.h" +#include "ui/image/image_prepare.h" +#include "media/streaming/media_streaming_instance.h" +#include "lang/lang_keys.h" +#include "styles/style_layers.h" +#include "styles/style_info.h" + +namespace { + +} // namespace + +PeerShortInfoBox::PeerShortInfoBox( + QWidget*, + PeerShortInfoType type, + rpl::producer fields, + rpl::producer status, + rpl::producer userpic) +: _type(type) +, _fields(std::move(fields)) +, _name(this, nameValue(), st::shortInfoName) +, _status(this, std::move(status), st::shortInfoStatus) { + std::move( + userpic + ) | rpl::start_with_next([=](PeerShortInfoUserpic &&value) { + applyUserpic(std::move(value)); + }, lifetime()); +} + +PeerShortInfoBox::~PeerShortInfoBox() = default; + +rpl::producer<> PeerShortInfoBox::openRequests() const { + return _openRequests.events(); +} + +void PeerShortInfoBox::prepare() { + addButton(tr::lng_close(), [=] { closeBox(); }); + + // Perhaps a new lang key should be added for opening a group. + addLeftButton((_type == PeerShortInfoType::User) + ? tr::lng_profile_send_message() + : (_type == PeerShortInfoType::Group) + ? tr::lng_view_button_group() + : tr::lng_profile_view_channel(), [=] { _openRequests.fire({}); }); + + setNoContentMargin(true); + setDimensions(st::shortInfoWidth, st::shortInfoWidth); +} + +RectParts PeerShortInfoBox::customCornersFilling() { + return RectPart::FullTop; +} + +void PeerShortInfoBox::resizeEvent(QResizeEvent *e) { + BoxContent::resizeEvent(e); +} + +void PeerShortInfoBox::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + + const auto coverSize = st::shortInfoWidth; + if (_userpicImage.isNull()) { + const auto size = coverSize * style::DevicePixelRatio(); + auto image = QImage(size, size, QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::black); + Images::prepareRound( + image, + ImageRoundRadius::Small, + RectPart::TopLeft | RectPart::TopRight); + } + p.drawImage(QRect(0, 0, coverSize, coverSize), _userpicImage); +} + +rpl::producer PeerShortInfoBox::nameValue() const { + return _fields.value() | rpl::map([](const PeerShortInfoFields &fields) { + return fields.name; + }) | rpl::distinct_until_changed(); +} + +void PeerShortInfoBox::applyUserpic(PeerShortInfoUserpic &&value) { + if (!value.photo.isNull() + && _userpicImage.cacheKey() != value.photo.cacheKey()) { + _userpicImage = std::move(value.photo); + update(); + } +} diff --git a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h new file mode 100644 index 0000000000..ade41a6c4f --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h @@ -0,0 +1,74 @@ +/* +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/abstract_box.h" + +namespace Media::Streaming { +class Document; +class Instance; +} // namespace Media::Streaming + +enum class PeerShortInfoType { + User, + Group, + Channel, +}; + +struct PeerShortInfoFields { + QString name; + QString phone; + QString link; + TextWithEntities about; + QString username; +}; + +struct PeerShortInfoUserpic { + int index = 0; + int count = 0; + + QImage photo; + float64 photoLoadingProgress = 0.; + std::shared_ptr videoDocument; +}; + +class PeerShortInfoBox final : public Ui::BoxContent { +public: + PeerShortInfoBox( + QWidget*, + PeerShortInfoType type, + rpl::producer fields, + rpl::producer status, + rpl::producer userpic); + ~PeerShortInfoBox(); + + [[nodiscard]] rpl::producer<> openRequests() const; + +private: + void prepare() override; + RectParts customCornersFilling() override; + + void resizeEvent(QResizeEvent *e) override; + void paintEvent(QPaintEvent *e) override; + + [[nodiscard]] rpl::producer nameValue() const; + void applyUserpic(PeerShortInfoUserpic &&value); + + const PeerShortInfoType _type = PeerShortInfoType::User; + + rpl::variable _fields; + + object_ptr _name; + object_ptr _status; + + QImage _userpicImage; + std::unique_ptr _videoInstance; + + rpl::event_stream<> _openRequests; + +}; diff --git a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp new file mode 100644 index 0000000000..daab8a1883 --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp @@ -0,0 +1,301 @@ +/* +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 "boxes/peers/prepare_short_info_box.h" + +#include "boxes/peers/peer_short_info_box.h" +#include "data/data_peer.h" +#include "data/data_photo.h" +#include "data/data_photo_media.h" +#include "data/data_streaming.h" +#include "data/data_file_origin.h" +#include "data/data_user.h" +#include "data/data_chat.h" +#include "data/data_channel.h" +#include "data/data_peer_values.h" +#include "data/data_changes.h" +#include "data/data_session.h" +#include "main/main_session.h" +#include "window/window_session_controller.h" +#include "info/profile/info_profile_values.h" +#include "ui/text/format_values.h" +#include "base/unixtime.h" +#include "lang/lang_keys.h" +#include "styles/style_info.h" + +namespace { + +struct UserpicState { + PeerShortInfoUserpic current; + std::shared_ptr userpicView; + std::shared_ptr photoView; + InMemoryKey userpicKey; + PhotoId photoId = 0; + bool waitingFull = false; + bool waitingLoad = false; +}; + +void GenerateImage( + not_null state, + QImage image, + bool blurred = false) { + using namespace Images; + const auto size = st::shortInfoWidth; + const auto factor = style::DevicePixelRatio(); + const auto options = Option::Smooth + | Option::RoundedSmall + | Option::RoundedTopLeft + | Option::RoundedTopRight + | (blurred ? Option::Blurred : Option()); + state->current.photo = Images::prepare( + std::move(image), + size * factor, + size * factor, + options, + size * factor, + size * factor); +} + +void GenerateImage( + not_null state, + not_null image, + bool blurred = false) { + GenerateImage(state, image->original(), blurred); +} + +void ProcessUserpic( + not_null peer, + not_null state) { + state->current.videoDocument = nullptr; + state->userpicKey = peer->userpicUniqueKey(state->userpicView); + if (!state->userpicView) { + GenerateImage( + state, + peer->generateUserpicImage( + state->userpicView, + st::shortInfoWidth * style::DevicePixelRatio(), + ImageRoundRadius::None), + false); + state->photoView = nullptr; + return; + } + peer->loadUserpic(); + state->current.photoLoadingProgress = 0.; + const auto image = state->userpicView->image(); + if (!image) { + state->current.photo = QImage(); + state->waitingLoad = true; + return; + } + GenerateImage(state, image, true); + state->photoView = nullptr; +} + +void ProcessFullPhoto( + not_null peer, + not_null state, + not_null photo) { + using PhotoSize = Data::PhotoSize; + const auto video = photo->hasVideo(); + const auto origin = peer->userpicPhotoOrigin(); + const auto was = base::take(state->current.videoDocument); + const auto view = photo->createMediaView(); + if (!video) { + view->wanted(PhotoSize::Large, origin); + } + if (const auto image = view->image(PhotoSize::Large)) { + GenerateImage(state, image); + state->photoView = nullptr; + state->current.photoLoadingProgress = 1.; + } else { + if (const auto thumbnail = view->image(PhotoSize::Thumbnail)) { + GenerateImage(state, thumbnail, true); + } else if (const auto small = view->image(PhotoSize::Small)) { + GenerateImage(state, small, true); + } else { + ProcessUserpic(peer, state); + if (state->current.photo.isNull()) { + if (const auto blurred = view->thumbnailInline()) { + GenerateImage(state, blurred, true); + } + } + } + state->waitingLoad = !video; + state->photoView = view; + state->current.photoLoadingProgress = photo->progress(); + } + if (!video) { + return; + } + state->current.videoDocument = peer->owner().streaming().sharedDocument( + photo, + origin); + state->photoView = nullptr; + state->current.photoLoadingProgress = 1.; +} + +} // namespace + +[[nodiscard]] rpl::producer FieldsValue( + not_null peer) { + using UpdateFlag = Data::PeerUpdate::Flag; + return peer->session().changes().peerFlagsValue( + peer, + (UpdateFlag::Name + | UpdateFlag::PhoneNumber + | UpdateFlag::Username + | UpdateFlag::About) + ) | rpl::map([=] { + const auto user = peer->asUser(); + const auto username = peer->userName(); + return PeerShortInfoFields{ + .name = peer->name, + .phone = user ? Ui::FormatPhone(user->phone()) : QString(), + .link = ((user || username.isEmpty()) + ? QString() + : peer->session().createInternalLinkFull(username)), + .about = Info::Profile::AboutWithEntities(peer, peer->about()), + .username = ((user && !username.isEmpty()) + ? ('@' + username) + : QString()) + }; + }); +} + +[[nodiscard]] rpl::producer StatusValue(not_null peer) { + if (const auto user = peer->asUser()) { + const auto now = base::unixtime::now(); + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + const auto timer = lifetime.make_state(); + const auto push = [=] { + consumer.put_next(Data::OnlineText(user, now)); + timer->callOnce(Data::OnlineChangeTimeout(user, now)); + }; + timer->setCallback(push); + push(); + return lifetime; + }; + } + return peer->session().changes().peerFlagsValue( + peer, + Data::PeerUpdate::Flag::Members + ) | rpl::map([=] { + const auto chat = peer->asChat(); + const auto channel = peer->asChannel(); + const auto count = std::max({ + chat ? chat->count : channel->membersCount(), + chat ? int(chat->participants.size()) : 0, + 0, + }); + return (chat && !chat->amIn()) + ? tr::lng_chat_status_unaccessible(tr::now) + : (count > 0) + ? ((channel && channel->isBroadcast()) + ? tr::lng_chat_status_subscribers( + tr::now, + lt_count_decimal, + count) + : tr::lng_chat_status_members( + tr::now, + lt_count_decimal, + count)) + : ((channel && channel->isBroadcast()) + ? tr::lng_channel_status(tr::now) + : tr::lng_group_status(tr::now)); + }); +} + +[[nodiscard]] rpl::producer UserpicValue( + not_null peer) { + return [=](auto consumer) { + auto lifetime = rpl::lifetime(); + const auto state = lifetime.make_state(); + const auto push = [=] { + state->waitingLoad = false; + const auto nowPhotoId = peer->userpicPhotoId(); + const auto photo = (nowPhotoId + && (nowPhotoId != PeerData::kUnknownPhotoId) + && (state->photoId != nowPhotoId || state->photoView)) + ? peer->owner().photo(nowPhotoId).get() + : nullptr; + state->waitingFull = !(state->photoId == nowPhotoId) + && ((nowPhotoId == PeerData::kUnknownPhotoId) + || (nowPhotoId && photo->isNull())); + if (state->waitingFull) { + peer->updateFullForced(); + } + const auto oldPhotoId = state->waitingFull + ? state->photoId + : std::exchange(state->photoId, nowPhotoId); + const auto changedPhotoId = (state->photoId != oldPhotoId); + const auto changedUserpic = (state->userpicKey + != peer->userpicUniqueKey(state->userpicView)); + if (!changedPhotoId + && !changedUserpic + && !state->photoView + && (!state->current.photo.isNull() + || state->current.videoDocument)) { + return; + } else if (photo && !photo->isNull()) { + ProcessFullPhoto(peer, state, photo); + } else { + ProcessUserpic(peer, state); + } + consumer.put_next_copy(state->current); + }; + using UpdateFlag = Data::PeerUpdate::Flag; + peer->session().changes().peerFlagsValue( + peer, + UpdateFlag::Photo | UpdateFlag::FullInfo + ) | rpl::filter([=](const Data::PeerUpdate &update) { + return (update.flags & UpdateFlag::Photo) || state->waitingFull; + }) | rpl::start_with_next([=] { + push(); + }, lifetime); + + peer->session().downloaderTaskFinished( + ) | rpl::filter([=] { + return state->waitingLoad + && (state->photoView + ? (!!state->photoView->image(Data::PhotoSize::Large)) + : (state->userpicView && state->userpicView->image())); + }) | rpl::start_with_next([=] { + push(); + }, lifetime); + + return lifetime; + }; +} + +object_ptr PrepareShortInfoBox( + not_null peer, + Fn open) { + const auto type = peer->isUser() + ? PeerShortInfoType::User + : peer->isBroadcast() + ? PeerShortInfoType::Channel + : PeerShortInfoType::Group; + auto result = Box( + type, + FieldsValue(peer), + StatusValue(peer), + UserpicValue(peer)); + + result->openRequests( + ) | rpl::start_with_next(open, result->lifetime()); + + return result; +} + +object_ptr PrepareShortInfoBox( + not_null peer, + not_null navigation) { + return PrepareShortInfoBox( + peer, + [=] { navigation->showPeerHistory(peer); }); +} diff --git a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.h b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.h new file mode 100644 index 0000000000..7494db61c2 --- /dev/null +++ b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.h @@ -0,0 +1,28 @@ +/* +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" + +class PeerData; + +namespace Ui { +class BoxContent; +} // namespace Ui + +namespace Window { +class SessionNavigation; +} // namespace Window + +[[nodiscard]] object_ptr PrepareShortInfoBox( + not_null peer, + Fn open); + +[[nodiscard]] object_ptr PrepareShortInfoBox( + not_null peer, + not_null navigation); diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index ffbead1a50..d7f66c957e 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -952,3 +952,9 @@ infoAboutGigagroup: FlatLabel(defaultFlatLabel) { infoScrollDateHideTimeout: historyScrollDateHideTimeout; infoDateFadeDuration: historyDateFadeDuration; + +shortInfoName: FlatLabel(defaultFlatLabel) { +} +shortInfoStatus: FlatLabel(defaultFlatLabel) { +} +shortInfoWidth: 336px; diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.cpp b/Telegram/SourceFiles/info/profile/info_profile_values.cpp index a4b377122d..248055dacd 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_values.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_values.cpp @@ -74,7 +74,7 @@ rpl::producer NameValue(not_null peer) { UpdateFlag::Name ) | rpl::map([=] { return peer->name; - }) | Ui::Text::ToWithEntities();; + }) | Ui::Text::ToWithEntities(); } rpl::producer PhoneValue(not_null user) { @@ -113,7 +113,9 @@ rpl::producer UsernameValue(not_null user) { }) | Ui::Text::ToWithEntities(); } -rpl::producer AboutValue(not_null peer) { +TextWithEntities AboutWithEntities( + not_null peer, + const QString &value) { auto flags = TextParseLinks | TextParseMentions; const auto user = peer->asUser(); const auto isBot = user && user->isBot(); @@ -125,15 +127,19 @@ rpl::producer AboutValue(not_null peer) { const auto stripExternal = peer->isChat() || peer->isMegagroup() || (user && !isBot); + auto result = TextWithEntities{ value }; + TextUtilities::ParseEntities(result, flags); + if (stripExternal) { + StripExternalLinks(result); + } + return result; +} + +rpl::producer AboutValue(not_null peer) { return PlainAboutValue( peer - ) | Ui::Text::ToWithEntities( - ) | rpl::map([=](TextWithEntities &&text) { - TextUtilities::ParseEntities(text, flags); - if (stripExternal) { - StripExternalLinks(text); - } - return std::move(text); + ) | rpl::map([peer](const QString &value) { + return AboutWithEntities(peer, value); }); } diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.h b/Telegram/SourceFiles/info/profile/info_profile_values.h index ef09ec80a8..cd35fae6a6 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_values.h +++ b/Telegram/SourceFiles/info/profile/info_profile_values.h @@ -38,6 +38,9 @@ rpl::producer NameValue(not_null peer); rpl::producer PhoneValue(not_null user); rpl::producer PhoneOrHiddenValue(not_null user); rpl::producer UsernameValue(not_null user); +[[nodiscard]] TextWithEntities AboutWithEntities( + not_null peer, + const QString &value); rpl::producer AboutValue(not_null peer); rpl::producer LinkValue(not_null peer); rpl::producer LocationValue(