From 360a92c198f0bc4f830be7d9b6e17322bba10c27 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 19 Oct 2021 18:48:10 +0400 Subject: [PATCH] Show user photos overview in PeerShortInfoBox. --- .../boxes/peers/peer_short_info_box.cpp | 135 ++++++++++++- .../boxes/peers/peer_short_info_box.h | 21 +- .../boxes/peers/prepare_short_info_box.cpp | 183 ++++++++++++++---- Telegram/SourceFiles/info/info.style | 3 + 4 files changed, 295 insertions(+), 47 deletions(-) diff --git a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp index dc696a4fcc..e31ea8c062 100644 --- a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.cpp @@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace { constexpr auto kShadowMaxAlpha = 80; +constexpr auto kInactiveBarOpacity = 0.5; } // namespace @@ -52,6 +53,11 @@ PeerShortInfoBox::PeerShortInfoBox( ) | rpl::start_with_next([=](PeerShortInfoUserpic &&value) { applyUserpic(std::move(value)); }, lifetime()); + + style::PaletteChanged( + ) | rpl::start_with_next([=] { + refreshBarImages(); + }, lifetime()); } PeerShortInfoBox::~PeerShortInfoBox() = default; @@ -60,6 +66,10 @@ rpl::producer<> PeerShortInfoBox::openRequests() const { return _openRequests.events(); } +rpl::producer PeerShortInfoBox::moveRequests() const { + return _moveRequests.events(); +} + void PeerShortInfoBox::prepare() { addButton(tr::lng_close(), [=] { closeBox(); }); @@ -79,6 +89,8 @@ void PeerShortInfoBox::prepare() { ) | rpl::start_with_next([=](int height) { setDimensions(st::shortInfoWidth, st::shortInfoWidth + height); }, lifetime()); + + setMouseTracking(true); } void PeerShortInfoBox::prepareRows() { @@ -143,7 +155,6 @@ void PeerShortInfoBox::prepareRows() { tr::lng_info_username_label(), usernameValue() | Ui::Text::ToWithEntities(), tr::lng_context_copy_mention(tr::now)); - } RectParts PeerShortInfoBox::customCornersFilling() { @@ -192,8 +203,72 @@ void PeerShortInfoBox::paintEvent(QPaintEvent *e) { _videoInstance->markFrameShown(); } - if (_shadow.isNull()) { - _shadow = Images::GenerateShadow( + paintBars(p); + paintShadow(p); +} + +void PeerShortInfoBox::mouseMoveEvent(QMouseEvent *e) { + //const auto x = e->pos().x(); + const auto cursor = (_count > 1) + ? style::cur_pointer + : style::cur_default; + //const auto cursor = (_index > 0 && x < st::shortInfoWidth / 3) + // ? style::cur_pointer + // : (_index + 1 < _count && x >= st::shortInfoWidth / 3) + // ? style::cur_pointer + // : style::cur_default; + if (_cursor != cursor) { + _cursor = cursor; + setCursor(_cursor); + } +} + +void PeerShortInfoBox::mousePressEvent(QMouseEvent *e) { + const auto x = e->pos().x(); + if (e->button() != Qt::LeftButton) { + return; + } else if (/*_index > 0 && */x < st::shortInfoWidth / 3) { + _moveRequests.fire(-1); + } else if (/*_index + 1 < _count && */x >= st::shortInfoWidth / 3) { + _moveRequests.fire(1); + } +} + +void PeerShortInfoBox::paintBars(QPainter &p) { + const auto height = st::shortInfoLinePadding * 2 + st::shortInfoLine; + if (_shadowTop.isNull()) { + _shadowTop = Images::GenerateShadow(height, kShadowMaxAlpha, 0); + } + const auto shadowRect = QRect(0, 0, st::shortInfoWidth, height); + const auto factor = style::DevicePixelRatio(); + p.drawImage( + shadowRect, + _shadowTop, + QRect(0, 0, _shadowTop.width(), height * factor)); + if (!_smallWidth) { + return; + } + const auto top = st::shortInfoLinePadding; + const auto skip = st::shortInfoLineSkip; + const auto full = (st::shortInfoWidth - 2 * top - (_count - 1) * skip); + const auto width = full / float64(_count); + for (auto i = 0; i != _count; ++i) { + const auto left = top + i * (width + skip); + const auto right = left + width; + p.setOpacity((i == _index) ? 1. : kInactiveBarOpacity); + p.drawImage( + qRound(left), + top, + ((qRound(right) == qRound(left) + _smallWidth) + ? _barSmall + : _barLarge)); + } + p.setOpacity(1.); +} + +void PeerShortInfoBox::paintShadow(QPainter &p) { + if (_shadowBottom.isNull()) { + _shadowBottom = Images::GenerateShadow( st::shortInfoShadowHeight, 0, kShadowMaxAlpha); @@ -206,11 +281,11 @@ void PeerShortInfoBox::paintEvent(QPaintEvent *e) { const auto factor = style::DevicePixelRatio(); p.drawImage( shadowRect, - _shadow, + _shadowBottom, QRect( 0, 0, - _shadow.width(), + _shadowBottom.width(), st::shortInfoShadowHeight * factor)); } @@ -261,14 +336,24 @@ rpl::producer PeerShortInfoBox::aboutValue() const { } void PeerShortInfoBox::applyUserpic(PeerShortInfoUserpic &&value) { + if (_index != value.index) { + _index = value.index; + update(); + } + if (_count != value.count) { + _count = value.count; + refreshBarImages(); + update(); + } if (!value.photo.isNull() && _userpicImage.cacheKey() != value.photo.cacheKey()) { _userpicImage = std::move(value.photo); update(); } - if (value.videoDocument - && (!_videoInstance - || _videoInstance->shared() != value.videoDocument)) { + if (!value.videoDocument) { + _videoInstance = nullptr; + } else if (!_videoInstance + || _videoInstance->shared() != value.videoDocument) { const auto frame = currentVideoFrame(); if (!frame.isNull()) { _userpicImage = frame; @@ -340,6 +425,40 @@ void PeerShortInfoBox::streamingReady(Media::Streaming::Information &&info) { update(coverRect()); } +void PeerShortInfoBox::refreshBarImages() { + if (_count < 2) { + _smallWidth = _largeWidth = 0; + _barSmall = _barLarge = QImage(); + return; + } + const auto width = st::shortInfoWidth - 2 * st::shortInfoLinePadding; + _smallWidth = (width - (_count - 1) * st::shortInfoLineSkip) / _count; + if (_smallWidth < st::shortInfoLine) { + _smallWidth = _largeWidth = 0; + _barSmall = _barLarge = QImage(); + return; + } + _largeWidth = _smallWidth + 1; + const auto makeBar = [](int size) { + const auto radius = st::shortInfoLine / 2.; + auto result = QImage( + QSize(size, st::shortInfoLine) * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(style::DevicePixelRatio()); + result.fill(Qt::transparent); + auto p = QPainter(&result); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(st::groupCallVideoTextFg); + p.drawRoundedRect(0, 0, size, st::shortInfoLine, radius, radius); + p.end(); + + return result; + }; + _barSmall = makeBar(_smallWidth); + _barLarge = makeBar(_largeWidth); +} + QRect PeerShortInfoBox::coverRect() const { return QRect(0, 0, st::shortInfoWidth, st::shortInfoWidth); } diff --git a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h index 06e701f0ef..a77eaf6fb1 100644 --- a/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h +++ b/Telegram/SourceFiles/boxes/peers/peer_short_info_box.h @@ -58,6 +58,7 @@ public: ~PeerShortInfoBox(); [[nodiscard]] rpl::producer<> openRequests() const; + [[nodiscard]] rpl::producer moveRequests() const; private: void prepare() override; @@ -66,6 +67,11 @@ private: void resizeEvent(QResizeEvent *e) override; void paintEvent(QPaintEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + + void paintBars(QPainter &p); + void paintShadow(QPainter &p); [[nodiscard]] QImage currentVideoFrame() const; @@ -84,6 +90,8 @@ private: void handleStreamingError(Media::Streaming::Error &&error); void streamingReady(Media::Streaming::Information &&info); + void refreshBarImages(); + const PeerShortInfoType _type = PeerShortInfoType::User; rpl::variable _fields; @@ -94,11 +102,22 @@ private: not_null _rows; QImage _userpicImage; + QImage _barSmall; + QImage _barLarge; + QImage _shadowTop; + int _smallWidth = 0; + int _largeWidth = 0; + int _index = 0; + int _count = 0; + + style::cursor _cursor = style::cur_default; + std::unique_ptr _videoInstance; crl::time _videoStartPosition = 0; Fn _videoPaused; - QImage _shadow; + QImage _shadowBottom; rpl::event_stream<> _openRequests; + rpl::event_stream _moveRequests; }; diff --git a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp index 865d230d08..bf713f1152 100644 --- a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp +++ b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp @@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_chat.h" #include "data/data_channel.h" #include "data/data_peer_values.h" +#include "data/data_user_photos.h" #include "data/data_changes.h" #include "data/data_session.h" #include "main/main_session.h" @@ -29,12 +30,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace { +constexpr auto kOverviewLimit = 48; + struct UserpicState { PeerShortInfoUserpic current; + std::optional userSlice; + PhotoId userpicPhotoId = PeerData::kUnknownPhotoId; std::shared_ptr userpicView; std::shared_ptr photoView; InMemoryKey userpicKey; - PhotoId photoId = 0; + PhotoId photoId = PeerData::kUnknownPhotoId; bool waitingFull = false; bool waitingLoad = false; }; @@ -117,8 +122,11 @@ void ProcessFullPhoto( } else if (const auto small = view->image(PhotoSize::Small)) { GenerateImage(state, small, true); } else { - ProcessUserpic(peer, state); - if (state->current.photo.isNull()) { + const auto current = (peer->userpicPhotoId() == photo->id); + if (current) { + ProcessUserpic(peer, state); + } + if (!current || state->current.photo.isNull()) { if (const auto blurred = view->thumbnailInline()) { GenerateImage(state, blurred, true); } @@ -212,43 +220,113 @@ void ProcessFullPhoto( }); } -[[nodiscard]] rpl::producer UserpicValue( - not_null peer) { - return [=](auto consumer) { +void ProcessOld(not_null peer, not_null state) { + +} + +void ValidatePhotoId( + not_null state, + PhotoId oldUserpicPhotoId) { + if (state->userSlice) { + const auto count = state->userSlice->size(); + const auto hasOld = state->userSlice->indexOf(oldUserpicPhotoId); + const auto hasNew = state->userSlice->indexOf(state->userpicPhotoId); + const auto shift = (hasNew ? 0 : 1); + const auto fullCount = count + shift; + state->current.count = fullCount; + if (hasOld && !hasNew && state->current.index + 1 < fullCount) { + ++state->current.index; + } else if (!hasOld && hasNew && state->current.index > 0) { + --state->current.index; + } + const auto index = state->current.index; + if (!index || index >= fullCount) { + state->current.index = 0; + state->photoId = state->userpicPhotoId; + } else { + state->photoId = (*state->userSlice)[index - shift]; + } + } else { + state->photoId = state->userpicPhotoId; + } +} + +bool ProcessCurrent( + not_null peer, + not_null state) { + const auto userpicPhotoId = peer->userpicPhotoId(); + const auto userpicPhoto = (userpicPhotoId + && (userpicPhotoId != PeerData::kUnknownPhotoId) + && (state->userpicPhotoId != userpicPhotoId)) + ? peer->owner().photo(userpicPhotoId).get() + : (state->photoId == userpicPhotoId && state->photoView) + ? state->photoView->owner().get() + : nullptr; + state->waitingFull = (state->userpicPhotoId != userpicPhotoId) + && ((userpicPhotoId == PeerData::kUnknownPhotoId) + || (userpicPhotoId && userpicPhoto->isNull())); + if (state->waitingFull) { + peer->updateFullForced(); + } + const auto oldUserpicPhotoId = state->waitingFull + ? state->userpicPhotoId + : std::exchange(state->userpicPhotoId, userpicPhotoId); + const auto changedUserpicPhotoId + = (state->userpicPhotoId != oldUserpicPhotoId); + const auto changedUserpic = (state->userpicKey + != peer->userpicUniqueKey(state->userpicView)); + + const auto wasIndex = state->current.index; + const auto wasCount = state->current.count; + const auto wasPhotoId = state->photoId; + ValidatePhotoId(state, oldUserpicPhotoId); + const auto changedInSlice = (state->current.index != wasIndex) + || (state->current.count != wasCount); + const auto changedPhotoId = (state->photoId != wasPhotoId); + const auto photo = (state->photoId == state->userpicPhotoId + && userpicPhoto) + ? userpicPhoto + : (state->photoId + && (state->photoId != PeerData::kUnknownPhotoId) + && changedPhotoId) + ? peer->owner().photo(state->photoId).get() + : state->photoView + ? state->photoView->owner().get() + : nullptr; + state->waitingLoad = false; + if (!changedPhotoId + && (state->current.index > 0 || !changedUserpic) + && !state->photoView + && (!state->current.photo.isNull() + || state->current.videoDocument)) { + return changedInSlice; + } else if (photo && !photo->isNull()) { + ProcessFullPhoto(peer, state, photo); + } else if (state->current.index > 0) { + return changedInSlice; + } else { + ProcessUserpic(peer, state); + } + return true; +} + +struct UserpicResult { + rpl::producer value; + Fn move; +}; + +[[nodiscard]] UserpicResult UserpicValue(not_null peer) { + const auto moveRequests = std::make_shared>(); + auto move = [=](int shift) { + moveRequests->fire_copy(shift); + }; + auto value = [=](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 push = [=](bool force = false) { + if (ProcessCurrent(peer, state) || force) { + consumer.put_next_copy(state->current); } - 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( @@ -260,6 +338,30 @@ void ProcessFullPhoto( push(); }, lifetime); + if (const auto user = peer->asUser()) { + UserPhotosReversedViewer( + &peer->session(), + UserPhotosSlice::Key(peerToUser(user->id), PhotoId()), + kOverviewLimit, + kOverviewLimit + ) | rpl::start_with_next([=](UserPhotosSlice &&slice) { + state->userSlice = std::move(slice); + push(); + }, lifetime); + } + + moveRequests->events( + ) | rpl::filter([=] { + return (state->current.count > 1); + }) | rpl::start_with_next([=](int shift) { + state->current.index = std::clamp( + ((state->current.index + shift + state->current.count) + % state->current.count), + 0, + state->current.count - 1); + push(true); + }, lifetime); + peer->session().downloaderTaskFinished( ) | rpl::filter([=] { return state->waitingLoad @@ -272,6 +374,7 @@ void ProcessFullPhoto( return lifetime; }; + return { .value = std::move(value), .move = std::move(move) }; } object_ptr PrepareShortInfoBox( @@ -283,16 +386,20 @@ object_ptr PrepareShortInfoBox( : peer->isBroadcast() ? PeerShortInfoType::Channel : PeerShortInfoType::Group; + auto userpic = UserpicValue(peer); auto result = Box( type, FieldsValue(peer), StatusValue(peer), - UserpicValue(peer), + std::move(userpic.value), std::move(videoPaused)); result->openRequests( ) | rpl::start_with_next(open, result->lifetime()); + result->moveRequests( + ) | rpl::start_with_next(userpic.move, result->lifetime()); + return result; } diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index ba072ce5e4..33f1135bd8 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -971,3 +971,6 @@ shortInfoNamePosition: point(25px, 37px); shortInfoStatusPosition: point(25px, 14px); shortInfoShadowHeight: 80px; shortInfoLabeledPadding: margins(20px, 16px, 20px, 0px); +shortInfoLinePadding: 8px; +shortInfoLineSkip: 4px; +shortInfoLine: 2px;