Show user photos overview in PeerShortInfoBox.

This commit is contained in:
John Preston 2021-10-19 18:48:10 +04:00
parent 64f6b86739
commit 360a92c198
4 changed files with 295 additions and 47 deletions

View File

@ -24,6 +24,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace { namespace {
constexpr auto kShadowMaxAlpha = 80; constexpr auto kShadowMaxAlpha = 80;
constexpr auto kInactiveBarOpacity = 0.5;
} // namespace } // namespace
@ -52,6 +53,11 @@ PeerShortInfoBox::PeerShortInfoBox(
) | rpl::start_with_next([=](PeerShortInfoUserpic &&value) { ) | rpl::start_with_next([=](PeerShortInfoUserpic &&value) {
applyUserpic(std::move(value)); applyUserpic(std::move(value));
}, lifetime()); }, lifetime());
style::PaletteChanged(
) | rpl::start_with_next([=] {
refreshBarImages();
}, lifetime());
} }
PeerShortInfoBox::~PeerShortInfoBox() = default; PeerShortInfoBox::~PeerShortInfoBox() = default;
@ -60,6 +66,10 @@ rpl::producer<> PeerShortInfoBox::openRequests() const {
return _openRequests.events(); return _openRequests.events();
} }
rpl::producer<int> PeerShortInfoBox::moveRequests() const {
return _moveRequests.events();
}
void PeerShortInfoBox::prepare() { void PeerShortInfoBox::prepare() {
addButton(tr::lng_close(), [=] { closeBox(); }); addButton(tr::lng_close(), [=] { closeBox(); });
@ -79,6 +89,8 @@ void PeerShortInfoBox::prepare() {
) | rpl::start_with_next([=](int height) { ) | rpl::start_with_next([=](int height) {
setDimensions(st::shortInfoWidth, st::shortInfoWidth + height); setDimensions(st::shortInfoWidth, st::shortInfoWidth + height);
}, lifetime()); }, lifetime());
setMouseTracking(true);
} }
void PeerShortInfoBox::prepareRows() { void PeerShortInfoBox::prepareRows() {
@ -143,7 +155,6 @@ void PeerShortInfoBox::prepareRows() {
tr::lng_info_username_label(), tr::lng_info_username_label(),
usernameValue() | Ui::Text::ToWithEntities(), usernameValue() | Ui::Text::ToWithEntities(),
tr::lng_context_copy_mention(tr::now)); tr::lng_context_copy_mention(tr::now));
} }
RectParts PeerShortInfoBox::customCornersFilling() { RectParts PeerShortInfoBox::customCornersFilling() {
@ -192,8 +203,72 @@ void PeerShortInfoBox::paintEvent(QPaintEvent *e) {
_videoInstance->markFrameShown(); _videoInstance->markFrameShown();
} }
if (_shadow.isNull()) { paintBars(p);
_shadow = Images::GenerateShadow( 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, st::shortInfoShadowHeight,
0, 0,
kShadowMaxAlpha); kShadowMaxAlpha);
@ -206,11 +281,11 @@ void PeerShortInfoBox::paintEvent(QPaintEvent *e) {
const auto factor = style::DevicePixelRatio(); const auto factor = style::DevicePixelRatio();
p.drawImage( p.drawImage(
shadowRect, shadowRect,
_shadow, _shadowBottom,
QRect( QRect(
0, 0,
0, 0,
_shadow.width(), _shadowBottom.width(),
st::shortInfoShadowHeight * factor)); st::shortInfoShadowHeight * factor));
} }
@ -261,14 +336,24 @@ rpl::producer<TextWithEntities> PeerShortInfoBox::aboutValue() const {
} }
void PeerShortInfoBox::applyUserpic(PeerShortInfoUserpic &&value) { 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() if (!value.photo.isNull()
&& _userpicImage.cacheKey() != value.photo.cacheKey()) { && _userpicImage.cacheKey() != value.photo.cacheKey()) {
_userpicImage = std::move(value.photo); _userpicImage = std::move(value.photo);
update(); update();
} }
if (value.videoDocument if (!value.videoDocument) {
&& (!_videoInstance _videoInstance = nullptr;
|| _videoInstance->shared() != value.videoDocument)) { } else if (!_videoInstance
|| _videoInstance->shared() != value.videoDocument) {
const auto frame = currentVideoFrame(); const auto frame = currentVideoFrame();
if (!frame.isNull()) { if (!frame.isNull()) {
_userpicImage = frame; _userpicImage = frame;
@ -340,6 +425,40 @@ void PeerShortInfoBox::streamingReady(Media::Streaming::Information &&info) {
update(coverRect()); 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 { QRect PeerShortInfoBox::coverRect() const {
return QRect(0, 0, st::shortInfoWidth, st::shortInfoWidth); return QRect(0, 0, st::shortInfoWidth, st::shortInfoWidth);
} }

View File

@ -58,6 +58,7 @@ public:
~PeerShortInfoBox(); ~PeerShortInfoBox();
[[nodiscard]] rpl::producer<> openRequests() const; [[nodiscard]] rpl::producer<> openRequests() const;
[[nodiscard]] rpl::producer<int> moveRequests() const;
private: private:
void prepare() override; void prepare() override;
@ -66,6 +67,11 @@ private:
void resizeEvent(QResizeEvent *e) override; void resizeEvent(QResizeEvent *e) override;
void paintEvent(QPaintEvent *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; [[nodiscard]] QImage currentVideoFrame() const;
@ -84,6 +90,8 @@ private:
void handleStreamingError(Media::Streaming::Error &&error); void handleStreamingError(Media::Streaming::Error &&error);
void streamingReady(Media::Streaming::Information &&info); void streamingReady(Media::Streaming::Information &&info);
void refreshBarImages();
const PeerShortInfoType _type = PeerShortInfoType::User; const PeerShortInfoType _type = PeerShortInfoType::User;
rpl::variable<PeerShortInfoFields> _fields; rpl::variable<PeerShortInfoFields> _fields;
@ -94,11 +102,22 @@ private:
not_null<Ui::VerticalLayout*> _rows; not_null<Ui::VerticalLayout*> _rows;
QImage _userpicImage; 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<Media::Streaming::Instance> _videoInstance; std::unique_ptr<Media::Streaming::Instance> _videoInstance;
crl::time _videoStartPosition = 0; crl::time _videoStartPosition = 0;
Fn<bool()> _videoPaused; Fn<bool()> _videoPaused;
QImage _shadow; QImage _shadowBottom;
rpl::event_stream<> _openRequests; rpl::event_stream<> _openRequests;
rpl::event_stream<int> _moveRequests;
}; };

View File

@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_chat.h" #include "data/data_chat.h"
#include "data/data_channel.h" #include "data/data_channel.h"
#include "data/data_peer_values.h" #include "data/data_peer_values.h"
#include "data/data_user_photos.h"
#include "data/data_changes.h" #include "data/data_changes.h"
#include "data/data_session.h" #include "data/data_session.h"
#include "main/main_session.h" #include "main/main_session.h"
@ -29,12 +30,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace { namespace {
constexpr auto kOverviewLimit = 48;
struct UserpicState { struct UserpicState {
PeerShortInfoUserpic current; PeerShortInfoUserpic current;
std::optional<UserPhotosSlice> userSlice;
PhotoId userpicPhotoId = PeerData::kUnknownPhotoId;
std::shared_ptr<Data::CloudImageView> userpicView; std::shared_ptr<Data::CloudImageView> userpicView;
std::shared_ptr<Data::PhotoMedia> photoView; std::shared_ptr<Data::PhotoMedia> photoView;
InMemoryKey userpicKey; InMemoryKey userpicKey;
PhotoId photoId = 0; PhotoId photoId = PeerData::kUnknownPhotoId;
bool waitingFull = false; bool waitingFull = false;
bool waitingLoad = false; bool waitingLoad = false;
}; };
@ -117,8 +122,11 @@ void ProcessFullPhoto(
} else if (const auto small = view->image(PhotoSize::Small)) { } else if (const auto small = view->image(PhotoSize::Small)) {
GenerateImage(state, small, true); GenerateImage(state, small, true);
} else { } else {
const auto current = (peer->userpicPhotoId() == photo->id);
if (current) {
ProcessUserpic(peer, state); ProcessUserpic(peer, state);
if (state->current.photo.isNull()) { }
if (!current || state->current.photo.isNull()) {
if (const auto blurred = view->thumbnailInline()) { if (const auto blurred = view->thumbnailInline()) {
GenerateImage(state, blurred, true); GenerateImage(state, blurred, true);
} }
@ -212,43 +220,113 @@ void ProcessFullPhoto(
}); });
} }
[[nodiscard]] rpl::producer<PeerShortInfoUserpic> UserpicValue( void ProcessOld(not_null<PeerData*> peer, not_null<UserpicState*> state) {
not_null<PeerData*> peer) {
return [=](auto consumer) { }
auto lifetime = rpl::lifetime();
const auto state = lifetime.make_state<UserpicState>(); void ValidatePhotoId(
const auto push = [=] { not_null<UserpicState*> state,
state->waitingLoad = false; PhotoId oldUserpicPhotoId) {
const auto nowPhotoId = peer->userpicPhotoId(); if (state->userSlice) {
const auto photo = (nowPhotoId const auto count = state->userSlice->size();
&& (nowPhotoId != PeerData::kUnknownPhotoId) const auto hasOld = state->userSlice->indexOf(oldUserpicPhotoId);
&& (state->photoId != nowPhotoId || state->photoView)) const auto hasNew = state->userSlice->indexOf(state->userpicPhotoId);
? peer->owner().photo(nowPhotoId).get() 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<PeerData*> peer,
not_null<UserpicState*> 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; : nullptr;
state->waitingFull = !(state->photoId == nowPhotoId) state->waitingFull = (state->userpicPhotoId != userpicPhotoId)
&& ((nowPhotoId == PeerData::kUnknownPhotoId) && ((userpicPhotoId == PeerData::kUnknownPhotoId)
|| (nowPhotoId && photo->isNull())); || (userpicPhotoId && userpicPhoto->isNull()));
if (state->waitingFull) { if (state->waitingFull) {
peer->updateFullForced(); peer->updateFullForced();
} }
const auto oldPhotoId = state->waitingFull const auto oldUserpicPhotoId = state->waitingFull
? state->photoId ? state->userpicPhotoId
: std::exchange(state->photoId, nowPhotoId); : std::exchange(state->userpicPhotoId, userpicPhotoId);
const auto changedPhotoId = (state->photoId != oldPhotoId); const auto changedUserpicPhotoId
= (state->userpicPhotoId != oldUserpicPhotoId);
const auto changedUserpic = (state->userpicKey const auto changedUserpic = (state->userpicKey
!= peer->userpicUniqueKey(state->userpicView)); != 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 if (!changedPhotoId
&& !changedUserpic && (state->current.index > 0 || !changedUserpic)
&& !state->photoView && !state->photoView
&& (!state->current.photo.isNull() && (!state->current.photo.isNull()
|| state->current.videoDocument)) { || state->current.videoDocument)) {
return; return changedInSlice;
} else if (photo && !photo->isNull()) { } else if (photo && !photo->isNull()) {
ProcessFullPhoto(peer, state, photo); ProcessFullPhoto(peer, state, photo);
} else if (state->current.index > 0) {
return changedInSlice;
} else { } else {
ProcessUserpic(peer, state); ProcessUserpic(peer, state);
} }
return true;
}
struct UserpicResult {
rpl::producer<PeerShortInfoUserpic> value;
Fn<void(int)> move;
};
[[nodiscard]] UserpicResult UserpicValue(not_null<PeerData*> peer) {
const auto moveRequests = std::make_shared<rpl::event_stream<int>>();
auto move = [=](int shift) {
moveRequests->fire_copy(shift);
};
auto value = [=](auto consumer) {
auto lifetime = rpl::lifetime();
const auto state = lifetime.make_state<UserpicState>();
const auto push = [=](bool force = false) {
if (ProcessCurrent(peer, state) || force) {
consumer.put_next_copy(state->current); consumer.put_next_copy(state->current);
}
}; };
using UpdateFlag = Data::PeerUpdate::Flag; using UpdateFlag = Data::PeerUpdate::Flag;
peer->session().changes().peerFlagsValue( peer->session().changes().peerFlagsValue(
@ -260,6 +338,30 @@ void ProcessFullPhoto(
push(); push();
}, lifetime); }, 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( peer->session().downloaderTaskFinished(
) | rpl::filter([=] { ) | rpl::filter([=] {
return state->waitingLoad return state->waitingLoad
@ -272,6 +374,7 @@ void ProcessFullPhoto(
return lifetime; return lifetime;
}; };
return { .value = std::move(value), .move = std::move(move) };
} }
object_ptr<Ui::BoxContent> PrepareShortInfoBox( object_ptr<Ui::BoxContent> PrepareShortInfoBox(
@ -283,16 +386,20 @@ object_ptr<Ui::BoxContent> PrepareShortInfoBox(
: peer->isBroadcast() : peer->isBroadcast()
? PeerShortInfoType::Channel ? PeerShortInfoType::Channel
: PeerShortInfoType::Group; : PeerShortInfoType::Group;
auto userpic = UserpicValue(peer);
auto result = Box<PeerShortInfoBox>( auto result = Box<PeerShortInfoBox>(
type, type,
FieldsValue(peer), FieldsValue(peer),
StatusValue(peer), StatusValue(peer),
UserpicValue(peer), std::move(userpic.value),
std::move(videoPaused)); std::move(videoPaused));
result->openRequests( result->openRequests(
) | rpl::start_with_next(open, result->lifetime()); ) | rpl::start_with_next(open, result->lifetime());
result->moveRequests(
) | rpl::start_with_next(userpic.move, result->lifetime());
return result; return result;
} }

View File

@ -971,3 +971,6 @@ shortInfoNamePosition: point(25px, 37px);
shortInfoStatusPosition: point(25px, 14px); shortInfoStatusPosition: point(25px, 14px);
shortInfoShadowHeight: 80px; shortInfoShadowHeight: 80px;
shortInfoLabeledPadding: margins(20px, 16px, 20px, 0px); shortInfoLabeledPadding: margins(20px, 16px, 20px, 0px);
shortInfoLinePadding: 8px;
shortInfoLineSkip: 4px;
shortInfoLine: 2px;